// ==UserScript== // @name BEST Cheat for Chess.com (NEW Stockfish 18.0.5 to 9.0, No Anti-Ban) // @namespace http://tampermonkey.net/ // @version 9.3.11 // @description An extremely advanced Chess.com cheat menu with 7 Stockfish models (18.0.5 to 9.0), tons of powerful features, and countless customization options. // @author Ech0 // @copyright 2025, Ech0 // @license MIT // @match https://www.chess.com/play/* // @match https://www.chess.com/game/* // @match https://www.chess.com/analysis // @match https://www.chess.com/analysis/* // @match https://www.chess.com/puzzles/* // @match https://www.chess.com/daily // @connect chess-api.com // @connect stockfish.online // @connect unpkg.com // @connect * // @grant GM_getResourceText // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @resource stockfish.js https://unpkg.com/stockfish@18.0.5/bin/stockfish-18-single.js // @run-at document-idle // @downloadURL https://update.greasyfork.icu/scripts/557044/BEST%20Cheat%20for%20Chesscom%20%28NEW%20Stockfish%201805%20to%2090%2C%20No%20Anti-Ban%29.user.js // @updateURL https://update.greasyfork.icu/scripts/557044/BEST%20Cheat%20for%20Chesscom%20%28NEW%20Stockfish%201805%20to%2090%2C%20No%20Anti-Ban%29.meta.js // ==/UserScript== (function () { "use strict"; // --- CONFIGURATION --- const CONFIG = { BOARD_SEL: "chess-board, wc-chess-board", LOOP_MS: 50, API: { MAX_DEPTH: 18, MAX_TIME: 2000 } }; const PIECE_IMGS = { p: "https://upload.wikimedia.org/wikipedia/commons/c/c7/Chess_pdt45.svg", r: "https://upload.wikimedia.org/wikipedia/commons/f/ff/Chess_rdt45.svg", n: "https://upload.wikimedia.org/wikipedia/commons/e/ef/Chess_ndt45.svg", b: "https://upload.wikimedia.org/wikipedia/commons/9/98/Chess_bdt45.svg", q: "https://upload.wikimedia.org/wikipedia/commons/4/47/Chess_qdt45.svg", k: "https://upload.wikimedia.org/wikipedia/commons/f/f0/Chess_kdt45.svg", P: "https://upload.wikimedia.org/wikipedia/commons/4/45/Chess_plt45.svg", R: "https://upload.wikimedia.org/wikipedia/commons/7/72/Chess_rlt45.svg", N: "https://upload.wikimedia.org/wikipedia/commons/7/70/Chess_nlt45.svg", B: "https://upload.wikimedia.org/wikipedia/commons/b/b1/Chess_blt45.svg", Q: "https://upload.wikimedia.org/wikipedia/commons/1/15/Chess_qlt45.svg", K: "https://upload.wikimedia.org/wikipedia/commons/4/42/Chess_klt45.svg", }; const STOCKFISH_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEGklEQVR4nO2ZW2gcVRjH/9+Z3ewm22xMNtVGU9RIxNqmFxF8sC0iFhF8UF980QcFL1jwaRELXnwQBC94UfBBEQtK0Yqi1LwgaL0k0DQm2zapm2az2d1kd2bO8f/M7Gw22U12052lB34wzMzO+Z/vO+d85ztnlkQIIYQQQgghhBBCSKtQSt1BCHmOEDKplLqD53n7x8fH9xBCfC2U0r2EkNcIIY/xPG9rIR4F8CGl9EEA+wghG5s9+yGl9F0A+9sKEEJ8B+A5AMcIIb6W/v8B4BCl9AkA+1oK8Ty/m1L6LID9hJCNzb7ZhRL6IoD9bQcopc8SQp4ghExt9mw/pfR5APtbcwH1C68W/l8B3wO463+xAOu5gH2EkG2EENSX8F4A+wkhG7mA+l3gVwD3tBCAUvoYIeQpQkh/s2f7KaVPAthfFw/4HsA+QsjGZt/sJ5Q+01oArvN9Qkh/s2f7KaWPE0L2112Au8D3AO4jhGxs9u0+SulTAPbXFfA9gP2EkI3NvttPKX0KwP66Ar4HsJ8QsrHZd/sppU8C2F9XwPcA9hNCNjb7bj+l9CkA++sK+B7AfYSQjc2+208pfQrA/rYClNI9hJCnCCHTmz3bTyl9CsD+tgOU0mcIIU8RQqY3e7afUvo0gP1tBSilz1BKnwGwv60A/H8uQAh5DsB+QsjGZt98Qil9DsD+1gKU0ucIIc8QQqY2e7afUvo8gP2tBaij0N8A7iOEbGz23X5K6fMA9tcV8D2A+wghG5t9t59S+iSA/XUFfA9gPyFkY7Pv9lNKTwLYX1fA9wDuI4RsbPbd/v8U4H/fA0II8Ty/mxDiA7C/Lh7wPID9hJCNzb7dTyl9EcD+ungA8Ty/mxDiA7C/pQCldC+l9EUA+1sK8Ty/hxDya0rpCwD2txTg/7kAIeR5APtbut8ghBBC2pZ/ALy683b5qZ2oAAAAAElFTkSuQmCC"; const DEFAULT_WASM_URL = "https://unpkg.com/stockfish@18.0.5/bin/stockfish-18-single.wasm"; // ─── Local Engine Registry ──────────────────────────────────────────────── // Each entry describes one loadable local engine variant. // // format: // "wasm-patched" → SF 18/17/16/15/14/12 style: fetch JS via GM_getResourceText // (or fall back to XHR if jsUrl is provided), fetch WASM via // GM_xmlhttpRequest, patch self.fetch, build Worker from Blob. // "asmjs" → SF 9/10 style: pure asm.js, load JS via XHR, // create Worker directly — no WASM involved at all. // // caps: // maxDepth → UI depth cap for this model // hasNNUE → whether UCI_LimitStrength/Elo is supported // hasSkillLevel → whether Skill Level UCI option exists // hasSlowMover → whether Slow Mover UCI option exists (removed in SF 16+) // hasWDL → whether UCI_ShowWDL is supported (SF 12+) // hasHash → Hash table option (all versions) // hasMoveOverhead → Move Overhead option (SF 14+, replaces Slow Mover in SF16) // // ── Local Engine Registry ──────────────────────────────────────────────── // Only 5 confirmed-working models. Each has a complete caps map so every // UCI option, UI row, and per-model saved setting is driven from here. // // format: "wasm" → JS + WASM fetched via XHR, self.fetch patched, Worker Blob // "asmjs" → single stockfish.js from cdnjs, Worker from JS text only // // Per-model settings are persisted under keys like "m_sf18_05_hashMB" so // each model remembers its own last-used values independently. const LOCAL_ENGINES = [ { id: "sf18_05", label: "Stockfish 18.0.5", cdn: "unpkg", format: "wasm", jsUrl: "https://unpkg.com/stockfish@18.0.5/bin/stockfish-18-single.js", wasmUrl: "https://unpkg.com/stockfish@18.0.5/bin/stockfish-18-single.wasm", // Capabilities maxDepth: 25, hasHash: true, hasMoveOverhead: true, // SF 14+ hasSlowMover: false, // removed in SF 16+ hasSkillLevel: true, hasNNUE: true, // UCI_LimitStrength + UCI_Elo hasWDL: true, // UCI_ShowWDL hasContempt: false, // removed in SF 12+ hasMinThink: true, // Per-model defaults defaults: { hashMB: 64, moveOverhead: 100, skillLevel: 20, limitStrength: false, elo: 3190, showWDL: false, minThinkTime: 20 }, }, { id: "sf16_00", label: "Stockfish 16.0", cdn: "unpkg", format: "wasm", jsUrl: "https://unpkg.com/stockfish@16.0.0/src/stockfish-nnue-16-single.js", wasmUrl: "https://unpkg.com/stockfish@16.0.0/src/stockfish-nnue-16-single.wasm", maxDepth: 25, hasHash: true, hasMoveOverhead: true, hasSlowMover: false, hasSkillLevel: true, hasNNUE: true, hasWDL: true, hasContempt: false, hasMinThink: true, defaults: { hashMB: 64, moveOverhead: 100, skillLevel: 20, limitStrength: false, elo: 3190, showWDL: false, minThinkTime: 20 }, }, { id: "sf11_00", label: "Stockfish 11.0", cdn: "unpkg", format: "wasm", jsUrl: "https://unpkg.com/stockfish@11.0.0/src/stockfish.js", wasmUrl: "https://unpkg.com/stockfish@11.0.0/src/stockfish.wasm", maxDepth: 20, hasHash: true, hasMoveOverhead: false, // added in SF 14 hasSlowMover: true, hasSkillLevel: true, hasNNUE: false, // classical HCE eval hasWDL: false, hasContempt: true, // present up to SF 11 hasMinThink: true, defaults: { hashMB: 32, slowMover: 100, skillLevel: 20, contempt: 24, minThinkTime: 20 }, }, { id: "sf10_02", label: "Stockfish 10.0.2 — asm.js", cdn: "cdnjs", format: "asmjs", jsUrl: "https://cdnjs.cloudflare.com/ajax/libs/stockfish.js/10.0.2/stockfish.js", wasmUrl: null, maxDepth: 20, hasHash: true, hasMoveOverhead: false, hasSlowMover: true, hasSkillLevel: true, hasNNUE: false, hasWDL: false, hasContempt: true, hasMinThink: true, defaults: { hashMB: 32, slowMover: 100, skillLevel: 20, contempt: 24, minThinkTime: 20 }, }, { id: "sf9_00", label: "Stockfish 9.0.0 — asm.js", cdn: "cdnjs", format: "asmjs", jsUrl: "https://cdnjs.cloudflare.com/ajax/libs/stockfish.js/9.0.0/stockfish.js", wasmUrl: null, maxDepth: 18, hasHash: true, hasMoveOverhead: false, hasSlowMover: true, hasSkillLevel: true, hasNNUE: false, hasWDL: false, hasContempt: true, hasMinThink: true, defaults: { hashMB: 16, slowMover: 100, skillLevel: 20, contempt: 24, minThinkTime: 20 }, }, ]; const getEngineById = (id) => LOCAL_ENGINES.find(e => e.id === id) || LOCAL_ENGINES[0]; const WASM_PRESETS = [{ label: "Custom URL...", url: "custom" }]; // legacy compat // --- STATE MANAGEMENT --- const state = { board: null, isThinking: !1, ui: {}, lastRawFEN: "N/A", lastSentFEN: "", lastSanitizedBoardFEN: "", lastMoveResult: "Waiting for analysis...", lastLiveResult: "Depth | Evaluation: Best move will appear here.", lastPayload: "N/A", lastResponse: "N/A", moveTargetTime: 0, calculatedDelay: 0, localEngine: null, engineLoadingInProgress: !1, engineStatus: "not_installed", engineStatusMsg: "", currentCloudRequest: null, currentBestMove: null, currentPV: [], analysisStartTime: 0, h: 180, s: 100, l: 50, newGameObserver: null, queueTimeout: null, localEval: null, localMate: null, localPV: null, localDepth: null, history: [], hasSavedCurrentGameResult: !1, lastSeenFEN: "", playingAs: null, visualTab: "move", visuals: [], }; const DEFAULT_SETTINGS = { engineMode: "cloud", depth: 18, moveKeybind: "None", maxThinkingTime: 0, searchMoves: "", autoRun: !0, autoMove: !0, autoQueue: !1, hideAfterMove: false, showPVArrows: !1, pvDepth: 5, pvShowNumbers: !1, pvCustomGradient: !1, pvStartColor: "#FFFF00", pvEndColor: "#FF0000", minDelay: 0, maxDelay: 0, highlightColor: "#00eeff", visualType: "outline", innerOpacity: 0.6, outerOpacity: 0.2, gradientBias: 0, arrowOpacity: 0.8, arrowWidth: 15, visualOutlineWidth: 5, visualOutlineOpacity: 0.5, visualOutlineGlow: !0, visualOutlineGlowRadius: 50, visualDuration: 0.6, visualFadeOut: !0, themeBg: "#222222", themeText: "#eeeeee", themeBorder: "#444444", themePrimary: "#81b64c", menuOpacity: 0.9, debugLogs: !1, enableHistory: !0, menuPosition: "top-right", localModelId: "sf18_05", // Per-model settings are stored under "m__" via GM_setValue. // These flat keys are only used as in-memory working copies (loaded on model select). localHashMB: 64, localMoveOverhead: 100, localSkillLevel: 20, localLimitStrength: false, localElo: 3190, localShowWDL: false, localMinThinkTime: 20, localSlowMover: 100, localContempt: 24, }; const settings = { ...DEFAULT_SETTINGS }; // --- COLOR HELPERS --- const hexToRgb = (hex) => { const r = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return r ? { r: parseInt(r[1], 16), g: parseInt(r[2], 16), b: parseInt(r[3], 16) } : { r: 0, g: 0, b: 0 }; }; const rgbToHex = (r, g, b) => "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); const rgbToHsl = (r, g, b) => { r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b), min = Math.min(r, g, b); let h, s, l = (max + min) / 2; if (max === min) h = s = 0; else { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return { h: h * 360, s: s * 100, l: l * 100 }; }; const hslToRgb = (h, s, l) => { let r, g, b; h /= 360; s /= 100; l /= 100; if (s === 0) r = g = b = l; else { const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }; }; // --- SAVE/LOAD HELPERS --- // ── Per-model setting helpers ──────────────────────────────────────────── // Global settings use "bot_". Per-model settings use "m__". // The flat settings.localXxx keys are working copies loaded from the active model. function saveSetting(key, val) { settings[key] = val; GM_setValue(`bot_${key}`, val); } // Save a per-model setting for the given (or current) model function saveModelSetting(key, val, modelId) { const mid = modelId || settings.localModelId || "sf18_05"; settings[key] = val; GM_setValue(`m_${mid}_${key}`, val); } // Load all per-model settings for a given model into settings.localXxx function loadModelSettings(modelId) { const m = getEngineById(modelId); const d = m.defaults; const g = (k, def) => { const v = GM_getValue(`m_${modelId}_${k}`); return v !== undefined ? v : def; }; settings.localHashMB = g("localHashMB", d.hashMB ?? 64); settings.localMoveOverhead = g("localMoveOverhead", d.moveOverhead ?? 100); settings.localSkillLevel = g("localSkillLevel", d.skillLevel ?? 20); settings.localLimitStrength = g("localLimitStrength",d.limitStrength ?? false); settings.localElo = g("localElo", d.elo ?? 3190); settings.localShowWDL = g("localShowWDL", d.showWDL ?? false); settings.localMinThinkTime = g("localMinThinkTime", d.minThinkTime ?? 20); settings.localSlowMover = g("localSlowMover", d.slowMover ?? 100); settings.localContempt = g("localContempt", d.contempt ?? 24); } function loadSettings() { Object.keys(DEFAULT_SETTINGS).forEach((k) => { const saved = GM_getValue(`bot_${k}`); if (saved !== undefined) settings[k] = saved; }); // Load per-model settings for the active model loadModelSettings(settings.localModelId || "sf18_05"); state.history = GM_getValue("bot_history", []); } // --- BOARD FEN LOGIC --- function getRawBoardFEN() { if (!state.board?.game) return null; try { if (typeof state.board.game.getFEN === "function") return state.board.game.getFEN(); if (typeof state.board.game.fen === "string") return state.board.game.fen; if (state.board.game.getPosition) return state.board.game.getPosition(); } catch (e) {} return null; } function sanitizeFEN(rawFEN) { if (!rawFEN) return ""; let parts = rawFEN.replace(/\s+/g, " ").trim().split(" "); if (parts.length < 6) { const def = ["w", "-", "-", "0", "1"]; for (let i = parts.length; i < 6; i++) parts.push(def[i - 1]); } if (parts[3] && parts[3] !== "-") parts[3] = parts[3].toLowerCase(); return parts.join(" "); } // --- VISUAL MANAGER --- const Visuals = { add: (move, type) => { if (!move) return; if (type === 'history') { Visuals.removeByType('history'); Visuals.removeByType('analysis'); } else if (type === 'analysis') { Visuals.removeByType('analysis'); } const id = `vis-${type}-${move}`; const existingIdx = state.visuals.findIndex(v => v.id === id); if (existingIdx !== -1) Visuals.remove(id); // NATIVE ARROW HANDLING if (settings.visualType === "nativeArrow") { state.board = document.querySelector(CONFIG.BOARD_SEL); if (state.board?.game?.markings) { state.board.game.markings.addOne({ type: "arrow", tags: ["Arrows", id], data: { from: move.substring(0, 2), to: move.substring(2, 4) } }); } state.visuals.push({ id, move, type, interval: null, isFading: false }); return; // Skips fading/DOM duration logic entirely } Visuals.draw(id, move); const interval = setInterval(() => { const vis = state.visuals.find(v => v.id === id); if (!vis || vis.isFading) { clearInterval(interval); return; } Visuals.draw(id, move); }, 50); state.visuals.push({ id, move, type, interval, isFading: false }); if (type === 'history') { if (settings.visualDuration === -1) { Visuals.remove(id); return; } if (settings.visualDuration > 0) { const ms = settings.visualDuration * 1000; if (settings.visualFadeOut) setTimeout(() => Visuals.fadeOut(id), ms); else setTimeout(() => Visuals.remove(id), ms); } } }, draw: (id, move) => { state.board = document.querySelector(CONFIG.BOARD_SEL); if (!state.board) return; const existing = document.querySelector(`.${id}`); if (existing) { if (!state.board.contains(existing)) existing.remove(); else return; } const { r, g, b } = hexToRgb(settings.highlightColor); const col = (a) => `rgba(${r}, ${g}, ${b}, ${a})`; const from = move.substring(0, 2); const to = move.substring(2, 4); const drawBox = () => { [from, to].forEach((alg) => { const sqId = `${alg.charCodeAt(0) - 96}${alg.charAt(1)}`; const div = document.createElement("div"); div.className = `square-${sqId} bot-highlight ${id}`; let baseStyle = `position: absolute; pointer-events: none !important; z-index: 1000000 !important; width: 12.5%; height: 12.5%; box-sizing: border-box; transition: none !important; `; if (settings.visualType === "outline") { let glow = settings.visualOutlineGlow ? `box-shadow: 0 0 ${settings.visualOutlineGlowRadius}px ${col(1)}, inset 0 0 ${settings.visualOutlineGlowRadius/2}px ${col(0.5)} !important;` : ""; div.style.cssText = baseStyle + `border: ${settings.visualOutlineWidth}px solid ${col(settings.visualOutlineOpacity)} !important; ${glow}`; } else { const bias = settings.gradientBias + "%"; div.style.cssText = baseStyle + `background: radial-gradient(closest-side, ${col(settings.innerOpacity)} ${bias}, ${col(settings.outerOpacity)} 100%) !important;`; } state.board.appendChild(div); }); }; if (settings.visualType === "arrow") drawArrow(move, id); else drawBox(); }, fadeOut: (id) => { const vis = state.visuals.find(v => v.id === id); if (!vis) return; vis.isFading = true; clearInterval(vis.interval); const els = document.querySelectorAll(`.${id}`); els.forEach(el => { el.style.setProperty("transition", `opacity ${settings.visualDuration}s linear`, "important"); el.style.setProperty("opacity", "0", "important"); }); setTimeout(() => Visuals.remove(id), settings.visualDuration * 1000); }, remove: (id) => { const idx = state.visuals.findIndex(v => v.id === id); if (idx !== -1) { clearInterval(state.visuals[idx].interval); state.visuals.splice(idx, 1); } document.querySelectorAll(`.${id}`).forEach(el => el.remove()); if (settings.visualType === "nativeArrow") { const board = document.querySelector(CONFIG.BOARD_SEL); if (board?.game?.markings) { board.game.markings.removeAll(); } } }, removeByType: (type) => { const toRemove = state.visuals.filter(v => v.type === type); toRemove.forEach(v => Visuals.remove(v.id)); } }; // --- PV MANAGER --- const PV = { interval: null, lastMoves: [], update: (pvMoves) => { PV.lastMoves = pvMoves || []; if (!settings.showPVArrows) { PV.clear(); return; } PV.draw(); if (!PV.interval) PV.interval = setInterval(PV.draw, 100); }, clear: () => { document.querySelectorAll('.pv-arrow').forEach(el => el.remove()); }, draw: () => { state.board = document.querySelector(CONFIG.BOARD_SEL); if (!state.board) return; if (!settings.showPVArrows || !PV.lastMoves.length) { PV.clear(); return; } const existing = document.querySelector('.pv-arrow'); if (existing && !state.board.contains(existing)) PV.clear(); const limit = Math.min(PV.lastMoves.length, settings.pvDepth); for (let i = 0; i < limit; i++) { const move = PV.lastMoves[i]; const id = `pv-arrow-${i}`; const el = document.querySelector(`.${id}`); if (el && state.board.contains(el)) { if (el.dataset.move === move) continue; el.remove(); } else if (el) el.remove(); let color = settings.highlightColor; if (settings.pvCustomGradient) { const start = hexToRgb(settings.pvStartColor); const end = hexToRgb(settings.pvEndColor); const factor = limit === 1 ? 0 : i / (limit - 1); const r = Math.round(start.r + factor * (end.r - start.r)); const g = Math.round(start.g + factor * (end.g - start.g)); const b = Math.round(start.b + factor * (end.b - start.b)); color = `rgb(${r},${g},${b})`; } drawPVArrow(move, id, color, i + 1); } let i = limit; while (document.querySelector(`.pv-arrow-${i}`)) { document.querySelectorAll(`.pv-arrow-${i}`).forEach(e => e.remove()); i++; } } }; // --- EVALUATION BAR --- // Inject a slight override to force the CSS grid to open up the eval column const injectEvalStyles = () => { if (document.getElementById('real-eval-styles')) return; const style = document.createElement('style'); style.id = 'real-eval-styles'; style.innerHTML = ` .board-layout-chessboard { grid-template-columns: [evaluation] 3rem [pieces] 0 [board] 1fr[board-controls] var(--boardControlsWidth) !important; column-gap: 1rem; } .evaluation-bar-color { transition: transform 1s ease-in !important; } /* Hide the old floating bar if it exists from previous script runs */ #bot-eval-bar { display: none !important; } `; document.head.appendChild(style); }; const EvalBar = { el: null, whiteFill: null, scoreAbbr: null, barInner: null, create: () => { injectEvalStyles(); // Force native classes on the body so Chess.com renders the grid properly if (!document.body.classList.contains('with-evaluation')) { document.body.classList.add('with-evaluation', 'with-visible-evaluation'); } // Find the native grid container const boardLayout = document.querySelector('.board-layout-chessboard'); if (!boardLayout) return; if (document.getElementById("custom-eval-wrapper")) { EvalBar.el = document.getElementById("custom-eval-wrapper"); EvalBar.barInner = document.querySelector(".evaluation-bar-bar"); EvalBar.whiteFill = document.getElementById("custom-eval-fill"); EvalBar.scoreAbbr = document.getElementById("custom-eval-abbr"); return; } // Create the wrapper using Chess.com's grid area class const wrapper = document.createElement('div'); wrapper.id = 'custom-eval-wrapper'; wrapper.className = 'board-layout-evaluation'; wrapper.style.cssText = 'display: flex; height: 100%; z-index: 10;'; // Exact DOM structure from Chess.com's React chunk wrapper.innerHTML = `
0.0
`; boardLayout.prepend(wrapper); EvalBar.el = wrapper; EvalBar.barInner = wrapper.querySelector(".evaluation-bar-bar"); EvalBar.whiteFill = document.getElementById("custom-eval-fill"); EvalBar.scoreAbbr = document.getElementById("custom-eval-abbr"); }, updatePosition: () => { // We no longer need to calculate bounding boxes! // Chess.com's native CSS grid handles the positioning and resizing automatically. }, update: (evalScore, mate) => { if (!EvalBar.el) EvalBar.create(); if (!EvalBar.whiteFill || !EvalBar.scoreAbbr) return; // Flip the bar if playing as black using Chess.com's native flip class const playingAsBlack = (state.playingAs === 2); if (playingAsBlack) { EvalBar.barInner.classList.add('evaluation-bar-flipped'); } else { EvalBar.barInner.classList.remove('evaluation-bar-flipped'); } let percentage = 50; let displayScore = "0.0"; if (mate !== null && mate !== undefined && mate !== 0) { const m = parseInt(mate); displayScore = "M" + Math.abs(m); // 0% translation = 100% white fill, 100% translation = 0% white fill percentage = m > 0 ? 0 : 100; } else if (evalScore !== null && evalScore !== undefined) { const ev = parseFloat(evalScore); displayScore = Math.abs(ev) >= 10 ? Math.round(ev).toString() : ev.toFixed(1); if (ev > 0 && displayScore !== "0.0") displayScore = "+" + displayScore; // Real Engine Sigmoid math to calculate visual percentage percentage = 50 - (Math.atan(ev / 2.5) / (Math.PI / 2) * 50); percentage = Math.max(5, Math.min(95, percentage)); // Cap it so it never fully vanishes } // Apply sliding transformation EvalBar.whiteFill.style.transform = `translate3d(0px, ${percentage.toFixed(2)}%, 0px)`; EvalBar.scoreAbbr.innerText = displayScore; // Adjust text color based on the bar's height overlaying the text if (percentage < 50) { EvalBar.scoreAbbr.className = 'evaluation-bar-scoreAbbreviated evaluation-bar-dark'; } else { EvalBar.scoreAbbr.className = 'evaluation-bar-scoreAbbreviated evaluation-bar-light'; } }, reset: () => { if (!EvalBar.el) return; if (EvalBar.whiteFill) EvalBar.whiteFill.style.transform = "translate3d(0px, 50%, 0px)"; if (EvalBar.scoreAbbr) { EvalBar.scoreAbbr.innerText = "0.0"; EvalBar.scoreAbbr.className = 'evaluation-bar-scoreAbbreviated evaluation-bar-dark'; } } }; function drawPVArrow(move, id, color, index) { if (!state.board) return; let isFlipped = state.board.classList.contains("flipped"); if (!isFlipped && state.board.game && state.board.game.getPlayingAs && state.board.game.getPlayingAs() === "b") isFlipped = true; const from = move.substring(0, 2), to = move.substring(2, 4); const getCoords = (sq) => { const file = sq.charCodeAt(0) - 97, rank = parseInt(sq[1]) - 1; return isFlipped ? { x: (7-file)*12.5+6.25, y: rank*12.5+6.25 } : { x: file*12.5+6.25, y: (7-rank)*12.5+6.25 }; }; const start = getCoords(from), end = getCoords(to); const dx = end.x - start.x, dy = end.y - start.y; const len = Math.sqrt(dx*dx + dy*dy); if (len === 0) return; const scale = (settings.arrowWidth || 15) / 15; const headLen = 4*scale, headWidth = 3*scale, lineWidth = 1.0*scale; const ux = dx/len, uy = dy/len; const endLineX = end.x - ux*headLen, endLineY = end.y - uy*headLen; const px = -uy, py = ux; const ns = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(ns, "svg"); svg.setAttribute("class", `pv-arrow ${id}`); svg.dataset.move = move; svg.style.cssText = "position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:900;"; svg.setAttribute("viewBox", "0 0 100 100"); const line = document.createElementNS(ns, "line"); line.setAttribute("x1", start.x); line.setAttribute("y1", start.y); line.setAttribute("x2", endLineX); line.setAttribute("y2", endLineY); line.setAttribute("stroke", color); line.setAttribute("stroke-width", lineWidth); line.setAttribute("stroke-opacity", settings.arrowOpacity || 0.8); line.setAttribute("stroke-linecap", "round"); const poly = document.createElementNS(ns, "polygon"); poly.setAttribute("points", `${end.x},${end.y} ${endLineX+px*(headWidth/2)},${endLineY+py*(headWidth/2)} ${endLineX-px*(headWidth/2)},${endLineY-py*(headWidth/2)}`); poly.setAttribute("fill", color); poly.setAttribute("fill-opacity", settings.arrowOpacity || 0.8); svg.appendChild(line); svg.appendChild(poly); if (settings.pvShowNumbers) { const text = document.createElementNS(ns, "text"); text.setAttribute("x", (start.x+end.x)/2); text.setAttribute("y", (start.y+end.y)/2); text.setAttribute("dy", "0.3em"); text.setAttribute("text-anchor", "middle"); text.setAttribute("fill", "#fff"); text.setAttribute("font-size", "2.5"); text.setAttribute("font-weight", "bold"); text.setAttribute("stroke", "#000"); text.setAttribute("stroke-width", "0.1"); text.textContent = index; svg.appendChild(text); } state.board.appendChild(svg); } function drawArrow(move, id) { const color = settings.highlightColor, opacity = settings.arrowOpacity, width = settings.arrowWidth; let isFlipped = !1; if (state.board.classList.contains("flipped")) isFlipped = !0; else if (state.board.game && state.board.game.getPlayingAs && state.board.game.getPlayingAs() === "b") isFlipped = !0; const from = move.substring(0, 2), to = move.substring(2, 4); const getCoords = (sq) => { const file = sq.charCodeAt(0) - 97, rank = parseInt(sq[1]) - 1; return isFlipped ? { x: (7-file)*12.5+6.25, y: rank*12.5+6.25 } : { x: file*12.5+6.25, y: (7-rank)*12.5+6.25 }; }; const start = getCoords(from), end = getCoords(to); const dx = end.x - start.x, dy = end.y - start.y; const len = Math.sqrt(dx*dx + dy*dy); if (len === 0) return; const scale = width/15, headLen = 4*scale, headWidth = 3*scale, lineWidth = 1.2*scale; const ux = dx/len, uy = dy/len; const endLineX = end.x - ux*headLen, endLineY = end.y - uy*headLen; const px = -uy, py = ux; const ns = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(ns, "svg"); svg.setAttribute("class", `bot-highlight ${id}`); svg.style.cssText = "position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:200;"; svg.setAttribute("viewBox", "0 0 100 100"); const line = document.createElementNS(ns, "line"); line.setAttribute("x1", start.x); line.setAttribute("y1", start.y); line.setAttribute("x2", endLineX); line.setAttribute("y2", endLineY); line.setAttribute("stroke", color); line.setAttribute("stroke-width", lineWidth); line.setAttribute("stroke-opacity", opacity); const polygon = document.createElementNS(ns, "polygon"); polygon.setAttribute("points", `${end.x},${end.y} ${endLineX+px*(headWidth/2)},${endLineY+py*(headWidth/2)} ${endLineX-px*(headWidth/2)},${endLineY-py*(headWidth/2)}`); polygon.setAttribute("fill", color); polygon.setAttribute("fill-opacity", opacity); svg.appendChild(line); svg.appendChild(polygon); state.board.appendChild(svg); } // --- EVAL STATUS LOGIC --- function getEvalStatusData(val, isMate) { const pa = state.playingAs || 1; let relativeScore = (pa === 2) ? -val : val; if (isMate) { if (relativeScore > 0) return { text: "Significant Advantage (Mate)", color: "#00ff00" }; return { text: "Significant Disadvantage (Mate)", color: "#ff0000" }; } if (relativeScore > 3) return { text: "Significant Advantage", color: "#00ff00" }; if (relativeScore > 1.5) return { text: "Clear Advantage", color: "#55ff55" }; if (relativeScore > 0.5) return { text: "Decisive Advantage", color: "#81b64c" }; if (relativeScore > 0.25) return { text: "Slight Advantage", color: "#aaffaa" }; if (relativeScore >= -0.25) return { text: "Equal", color: "#aaaaaa" }; if (relativeScore >= -0.5) return { text: "Slight Disadvantage", color: "#ffaaaa" }; if (relativeScore >= -1.5) return { text: "Decisive Disadvantage", color: "#ff7777" }; if (relativeScore >= -3) return { text: "Clear Disadvantage", color: "#ff4444" }; return { text: "Significant Disadvantage", color: "#ff0000" }; } // --- SF18 ENGINE CORE --- function setEngineStatus(status, msg) { state.engineStatus = status; state.engineStatusMsg = msg || ""; updateLocalSettingsUI(); } // ─── MULTI-MODEL ENGINE CORE ────────────────────────────────────────────── // Send UCI init commands appropriate for the selected engine model function sendEngineInitCommands(eng) { const m = getEngineById(eng || settings.localModelId); const cmds = ["ucinewgame"]; if (m.hasHash) cmds.push(`setoption name Hash value ${settings.localHashMB}`); if (m.hasMoveOverhead) cmds.push(`setoption name Move Overhead value ${settings.localMoveOverhead}`); if (m.hasSlowMover) cmds.push(`setoption name Slow Mover value ${settings.localSlowMover}`); if (m.hasMinThink) cmds.push(`setoption name Minimum Thinking Time value ${settings.localMinThinkTime}`); if (m.hasWDL) cmds.push(`setoption name UCI_ShowWDL value ${settings.localShowWDL}`); if (m.hasSkillLevel) cmds.push(`setoption name Skill Level value ${settings.localSkillLevel}`); if (m.hasNNUE) { cmds.push(`setoption name UCI_LimitStrength value ${settings.localLimitStrength}`); cmds.push(`setoption name UCI_Elo value ${settings.localElo}`); } if (m.hasContempt) cmds.push(`setoption name Contempt value ${settings.localContempt}`); cmds.push("setoption name MultiPV value 1"); cmds.forEach(c => state.localEngine.postMessage(c)); } // Build a Worker from a patched JS blob (for WASM-based engines) function buildWasmPatchedEngine(jsCode, wasmBytes) { let bin = ""; const chunk = 8192; for (let i = 0; i < wasmBytes.length; i += chunk) bin += String.fromCharCode.apply(null, wasmBytes.subarray(i, i + chunk)); const wasmB64 = btoa(bin); const patchCode = ` var _wasmB64 = "${wasmB64}"; var _wasmBytes = (function(){ var b = atob(_wasmB64), a = new Uint8Array(b.length); for(var i=0;i { const db = e.target.result; if (!db.objectStoreNames.contains("engines")) db.createObjectStore("engines"); }; dbReq.onsuccess = (e) => cb(null, e.target.result); dbReq.onerror = () => cb(new Error("IndexedDB open failed"), null); } function readCache(db, key, cb) { try { const req = db.transaction("engines", "readonly").objectStore("engines").get(key); req.onsuccess = (e) => cb(null, e.target.result || null); req.onerror = () => cb(null, null); } catch (e) { cb(null, null); } } function writeCache(db, key, data) { try { const tx = db.transaction("engines", "readwrite"); tx.objectStore("engines").put(data, key); } catch (e) {} } function deleteCache(db, key, cb) { try { const tx = db.transaction("engines", "readwrite"); const req = tx.objectStore("engines").delete(key); tx.oncomplete = () => cb && cb(); req.onerror = () => cb && cb(); } catch (e) { cb && cb(); } } // ─── Download helpers ───────────────────────────────────────────────────── function xhrText(url, cb, errCb) { GM_xmlhttpRequest({ method: "GET", url, timeout: 30000, onload: (r) => { if (r.status >= 400) { errCb(new Error(`HTTP ${r.status}`)); return; } cb(r.responseText); }, onerror: (e) => errCb(new Error("Network error: " + url)), ontimeout: () => errCb(new Error("Timeout: " + url)), }); } function xhrBinary(url, cb, errCb) { GM_xmlhttpRequest({ method: "GET", url, responseType: "arraybuffer", timeout: 60000, onload: (r) => { if (r.status >= 400) { errCb(new Error(`HTTP ${r.status}`)); return; } cb(new Uint8Array(r.response)); }, onerror: (e) => errCb(new Error("Binary download failed: " + url)), ontimeout: () => errCb(new Error("Binary timeout: " + url)), }); } // ─── Main load entry point ──────────────────────────────────────────────── function loadLocalEngine() { if (state.localEngine || state.engineLoadingInProgress) return; state.engineLoadingInProgress = true; state.isThinking = false; const modelId = settings.localModelId || "sf18_05"; const m = getEngineById(modelId); const label = m.format === "asmjs" ? `${m.label} (asm.js)` : m.label; setEngineStatus("loading", "Checking cache..."); state.lastMoveResult = `⏳ Loading ${label}...`; updateUI(); openCache((dbErr, db) => { if (m.format === "asmjs") { // ── asm.js path: XHR the JS text, build Worker directly ────── const launch = (jsCode) => { try { state.localEngine = buildAsmJsEngine(jsCode); state.localEngine.onerror = onEngineWorkerError; state.localEngine.onmessage = handleLocalMessage; finalizeEngine(modelId); } catch (e) { state.engineLoadingInProgress = false; setEngineStatus("error", e.message || "Build failed"); } }; if (db) { readCache(db, m.cacheKey, (_, cached) => { if (cached) { setEngineStatus("loading", "Loading from cache..."); launch(cached); } else { setEngineStatus("loading", "Downloading JS..."); xhrText(m.jsUrl, (js) => { if (db) writeCache(db, m.cacheKey, js); launch(js); }, (e) => { state.engineLoadingInProgress = false; setEngineStatus("error", "Download failed: " + e.message); } ); } }); } else { xhrText(m.jsUrl, launch, (e) => { state.engineLoadingInProgress = false; setEngineStatus("error", e.message); }); } } else { // ── wasm format: cache BOTH js text and wasm bytes in IndexedDB ── // Keys: m.cacheKey + "_js" for the JS text, m.cacheKey + "_wasm" for bytes. // SF18.0.5: try @resource first for JS (already bundled), avoids any XHR. const jsKey = m.cacheKey + "_js"; const wasmKey = m.cacheKey + "_wasm"; const launch = (jsCode, wasmBytes) => { try { state.localEngine = buildWasmPatchedEngine(jsCode, wasmBytes); state.localEngine.onerror = onEngineWorkerError; state.localEngine.onmessage = handleLocalMessage; finalizeEngine(modelId); } catch (e) { state.engineLoadingInProgress = false; setEngineStatus("error", e.message || "Build failed"); } }; // Step 1: get JS (from @resource, IndexedDB cache, or XHR) const getJs = (cb) => { // SF18.0.5: @resource is bundled — fastest, no network hit const bundled = GM_getResourceText("stockfish.js"); if (bundled) { cb(bundled); return; } // Try IndexedDB cache if (db) { readCache(db, jsKey, (_, cachedJs) => { if (cachedJs) { cb(cachedJs); } else { setEngineStatus("loading", "Downloading JS..."); xhrText(m.jsUrl, (js) => { writeCache(db, jsKey, js); cb(js); }, (e) => { state.engineLoadingInProgress = false; setEngineStatus("error", "JS download failed: " + e.message); } ); } }); } else { setEngineStatus("loading", "Downloading JS..."); xhrText(m.jsUrl, cb, (e) => { state.engineLoadingInProgress = false; setEngineStatus("error", "JS download failed: " + e.message); } ); } }; // Step 2: get WASM (from IndexedDB cache or XHR) const getWasm = (jsCode, cb) => { if (db) { readCache(db, wasmKey, (_, cachedWasm) => { if (cachedWasm) { setEngineStatus("loading", "Loading from cache..."); cb(jsCode, cachedWasm); } else { setEngineStatus("loading", "Downloading WASM..."); xhrBinary(m.wasmUrl, (bytes) => { writeCache(db, wasmKey, bytes); cb(jsCode, bytes); }, (e) => { state.engineLoadingInProgress = false; setEngineStatus("error", "WASM download failed: " + e.message); } ); } }); } else { setEngineStatus("loading", "Downloading WASM..."); xhrBinary(m.wasmUrl, (bytes) => cb(jsCode, bytes), (e) => { state.engineLoadingInProgress = false; setEngineStatus("error", e.message); } ); } }; getJs((jsCode) => { if (!jsCode) return; getWasm(jsCode, launch); }); } }); } function reinstallEngine() { if (state.localEngine) { try { state.localEngine.terminate(); } catch (e) {} state.localEngine = null; } state.engineLoadingInProgress = false; const m = getEngineById(settings.localModelId); setEngineStatus("loading", "Clearing cache..."); openCache((dbErr, db) => { if (!db) { loadLocalEngine(); return; } // Delete both JS and WASM cache entries, then reload deleteCache(db, m.cacheKey + "_js", () => deleteCache(db, m.cacheKey + "_wasm", () => deleteCache(db, m.cacheKey, () => loadLocalEngine()) // legacy key too ) ); }); } function uninstallEngine() { if (state.localEngine) { try { state.localEngine.terminate(); } catch (e) {} state.localEngine = null; } state.engineLoadingInProgress = false; const m = getEngineById(settings.localModelId); openCache((dbErr, db) => { if (!db) { setEngineStatus("not_installed", ""); return; } deleteCache(db, m.cacheKey + "_js", () => deleteCache(db, m.cacheKey + "_wasm", () => deleteCache(db, m.cacheKey, () => setEngineStatus("not_installed", "")) ) ); }); state.lastMoveResult = "Local engine uninstalled."; updateUI(); } function triggerFallback() { if (settings.engineMode === 'local') return; console.warn(`API Error. Switching to Local SF18 at Depth ${settings.depth}.`); settings.engineMode = 'local'; saveSetting('engineMode', 'local'); if (state.ui.selMode) state.ui.selMode.value = 'local'; state.lastMoveResult = `⚠️ API Error. Switched to Local SF18.`; loadLocalEngine(); if (state.lastSanitizedBoardFEN) analyzeLocal(state.lastSanitizedBoardFEN, settings.depth); updateUI(); } function analyze(depth = settings.depth, fenOverride = null, isRetry = !1) { if (state.isThinking && !fenOverride && !isRetry) return; let finalFEN = fenOverride || sanitizeFEN(getRawBoardFEN()); if (!finalFEN) return; state.lastRawFEN = finalFEN; state.lastSentFEN = finalFEN; if (!fenOverride) state.lastSanitizedBoardFEN = finalFEN; state.isThinking = !0; state.analysisStartTime = performance.now(); const minMs = settings.minDelay * 1000, maxMs = settings.maxDelay * 1000; const delay = Math.random() * (maxMs - minMs) + minMs; state.moveTargetTime = performance.now() + delay; state.calculatedDelay = (delay / 1000).toFixed(2); updateUI(); if (settings.engineMode === "cloud") analyzeCloud(finalFEN, depth, isRetry); else if (settings.engineMode === "sfonline") analyzeSF16(finalFEN, depth); else analyzeLocal(finalFEN, depth); } function analyzeCloud(finalFEN, depth, isRetry) { const actualDepth = Math.min(depth, 18); const payload = { fen: finalFEN, depth: actualDepth, maxThinkingTime: Math.min(settings.maxThinkingTime, CONFIG.API.MAX_TIME), taskId: Math.random().toString(36).substring(7), }; if (settings.searchMoves.trim()) payload.searchmoves = settings.searchMoves.trim(); state.lastPayload = `POST https://chess-api.com/v1\n${JSON.stringify(payload, null, 2)}`; if (state.ui.liveOutput) state.ui.liveOutput.innerHTML = isRetry ? "♻️ Retrying Safe FEN..." : "☁️ SF18 Cloud Analysis..."; updateUI(); state.currentCloudRequest = GM_xmlhttpRequest({ method: "POST", url: "https://chess-api.com/v1", headers: { "Content-Type": "application/json" }, data: JSON.stringify(payload), timeout: 15000, onload: (res) => handleCloudResponse(res, finalFEN, actualDepth, isRetry), onerror: (err) => { handleError("Network Error", err); triggerFallback(); }, ontimeout: () => { handleError("Timeout (15s)"); triggerFallback(); }, }); } function analyzeSF16(finalFEN, depth) { const actualDepth = Math.min(depth, 15); const url = `https://stockfish.online/api/s/v2.php?fen=${encodeURIComponent(finalFEN)}&depth=${actualDepth}&mode=bestmove`; state.lastPayload = `GET ${url}`; if (state.ui.liveOutput) state.ui.liveOutput.innerHTML = "☁️ SF17.1.0 Analysis..."; updateUI(); state.currentCloudRequest = GM_xmlhttpRequest({ method: "GET", url, timeout: 20000, onload: (res) => handleSF16Response(res), onerror: (err) => { handleError("Network Error (SF16)", err); triggerFallback(); }, ontimeout: () => { handleError("Timeout (SF16 20s)"); triggerFallback(); }, }); } function handleSF16Response(response) { state.isThinking = !1; state.lastResponse = response.responseText; try { if (response.status !== 200) throw new Error(`HTTP ${response.status}`); const data = JSON.parse(response.responseText); if (!data.success || !data.bestmove) { triggerFallback(); return; } const bestMove = data.bestmove.split(" ")[1] || data.bestmove; const duration = ((performance.now() - state.analysisStartTime) / 1000).toFixed(2); processBestMove(bestMove, data.evaluation, data.mate, data.continuation ? data.continuation.split(" ") : null, null, duration, true); } catch (e) { triggerFallback(); } updateUI(); } function handleCloudResponse(response, sentFEN, depth, isRetry) { state.isThinking = !1; state.lastResponse = response.responseText; if (response.responseText.includes("HIGH_USAGE") || response.status === 429) { triggerFallback(); return; } try { if (response.status !== 200) throw new Error(`HTTP ${response.status}`); const rawData = JSON.parse(response.responseText); const result = Array.isArray(rawData) ? rawData[0] : rawData; if (!result || result.error || result.status === "error") { const errText = result?.error || result?.message || "Unknown Error"; if (errText.includes("HIGH_USAGE")) { triggerFallback(); return; } if ((errText.includes("FEN") || errText.includes("VALIDATION")) && !isRetry) { const parts = sentFEN.split(" "); if (parts.length >= 4 && parts[3] !== "-") { parts[3] = "-"; analyze(depth, parts.join(" "), !0); return; } } triggerFallback(); return; } if (result.move || result.bestmove) { const duration = ((performance.now() - state.analysisStartTime) / 1000).toFixed(2); processBestMove(result.move || result.bestmove, result.eval, result.mate, result.continuationArr, result.winChance, duration, true); } else { triggerFallback(); } } catch (e) { triggerFallback(); } updateUI(); } function analyzeLocal(fen, depth) { if (!state.localEngine) { loadLocalEngine(); return; } state.localEval = null; state.localMate = null; state.localPV = null; state.localDepth = null; const m = getEngineById(settings.localModelId); const actualDepth = Math.min(depth, m.maxDepth); const cmds = [`position fen ${fen}`, `go depth ${actualDepth}`]; state.lastPayload = `Worker CMDs:\n${cmds.join("\n")}`; state.ui.liveOutput.innerHTML = "⚡ Local SF18 Analysis..."; updateUI(); cmds.forEach((cmd) => state.localEngine.postMessage(cmd)); } function handleLocalMessage(e) { const msg = typeof e.data === "string" ? e.data : (e.data?.toString ? e.data.toString() : null); if (!msg || typeof msg !== "string") return; state.lastResponse = (state.lastResponse.length > 500 ? "..." + state.lastResponse.slice(-500) : state.lastResponse) + "\n" + msg; if (state.ui.logRec) state.ui.logRec.innerText = state.lastResponse; // Engine signals it's ready — flip status immediately if (msg === "uciok" || msg.startsWith("uciok")) { if (state.engineStatus !== "ready") { const m = getEngineById(settings.localModelId || "sf18_05"); setEngineStatus("ready", ""); state.lastMoveResult = `✅ ${m.label} ready.`; updateUI(); } return; } if (msg === "readyok") { if (state.engineStatus !== "ready") { const m = getEngineById(settings.localModelId || "sf18_05"); setEngineStatus("ready", ""); state.lastMoveResult = `✅ ${m.label} ready.`; updateUI(); } return; } if (msg.startsWith("info") && msg.includes("depth") && msg.includes("score")) { const depthMatch = msg.match(/depth (\d+)/); const scoreMatch = msg.match(/score (cp|mate) (-?\d+)/); const pvMatch = msg.match(/ pv (.*)/); if (depthMatch && scoreMatch) { const depth = depthMatch[1]; let val = parseInt(scoreMatch[2]); const type = scoreMatch[1]; const fenParts = state.lastSentFEN ? state.lastSentFEN.split(" ") : []; const sideToMove = fenParts.length > 1 ? fenParts[1] : "w"; if (sideToMove === "b") val = -val; const pv = pvMatch ? pvMatch[1] : ""; if (type === "mate") { state.localMate = val; state.localEval = null; } else { state.localMate = null; state.localEval = (val / 100).toFixed(2); } state.localPV = pv; state.localDepth = depth; if (pv) state.currentPV = pv.split(" "); EvalBar.update(type === "mate" ? null : parseFloat(state.localEval), type === "mate" ? val : null); let scoreTxt; if (type === "mate") { scoreTxt = "M" + Math.abs(val); if (val < 0) scoreTxt = "-" + scoreTxt; } else { scoreTxt = (val > 0 ? "+" : "") + (val / 100).toFixed(2); } const evalVal = type === "mate" ? val : parseFloat(state.localEval); const statusData = getEvalStatusData(evalVal, type === "mate"); const duration = ((performance.now() - state.analysisStartTime) / 1000).toFixed(2); if (pv) { const best = pv.split(" ")[0]; Visuals.add(best, 'analysis'); PV.update(state.currentPV); state.lastMoveResult = `⏳ D${depth}: ${best}`; } state.lastLiveResult = `
${scoreTxt} ${statusData.text}
(${duration}s)
`; updateUI(); } } if (msg.startsWith("bestmove")) { state.isThinking = !1; const parts = msg.split(" "); const bestMove = parts[1]; if (bestMove && bestMove !== "(none)") { const duration = ((performance.now() - state.analysisStartTime) / 1000).toFixed(2); processBestMove(bestMove, state.localEval, state.localMate, state.localPV ? state.localPV.split(" ") : null, null, duration, state.localDepth, true); } else state.lastMoveResult = "⚠️ No move found"; updateUI(); } } function processBestMove(bestMove, evalScore, mate, continuationArr, winChance, duration, depth = null, isFinal = false) { state.currentBestMove = bestMove; state.currentPV = continuationArr || (bestMove ? [bestMove] : []); if (isFinal || !state.isThinking) { Visuals.add(bestMove, 'history'); PV.clear(); } else { Visuals.add(bestMove, 'analysis'); PV.update(state.currentPV); } const evalNum = (evalScore !== null && evalScore !== undefined) ? parseFloat(evalScore) : null; const mateNum = (mate !== null && mate !== undefined && mate !== 0) ? parseInt(mate) : null; EvalBar.update(evalNum, mateNum); let scoreTxt = "", pvStr = "N/A", numericValForStatus = 0, isMate = false; if (evalScore !== undefined || mate !== undefined) { if (mate) { isMate = true; numericValForStatus = mate; scoreTxt = `M${Math.abs(mate)}`; if (mate < 0) scoreTxt = "-" + scoreTxt; } else { const sc = parseFloat(evalScore); numericValForStatus = sc; scoreTxt = (sc > 0 ? "+" : "") + sc; } if (continuationArr) pvStr = continuationArr.join(" "); } const statusData = getEvalStatusData(numericValForStatus, isMate); const durHtml = duration ? `(${duration}s)` : ""; state.lastMoveResult = `✅ Best: ${bestMove}`; let wcHtml = ""; if (winChance) wcHtml = `(${Math.round(winChance)}%)`; else if (depth) wcHtml = `(D${depth})`; state.lastLiveResult = `
${scoreTxt} ${statusData.text}
${wcHtml} ${durHtml}
PV: ${pvStr}
`; if (settings.autoMove) triggerAutoMove(); } function triggerAutoMove() { if (!state.currentBestMove || !state.board?.game) return; const turn = state.board.game.getTurn(); const playingAs = state.board.game.getPlayingAs(); if (turn !== playingAs) return; const wait = Math.max(0, state.moveTargetTime - performance.now()); setTimeout(() => playMove(state.currentBestMove), wait); } function handleError(type, err) { state.isThinking = !1; console.error(type, err); state.lastResponse = `${type}: ${err?.message || err}`; state.lastMoveResult = `❌ ${type}`; updateUI(); } function playMove(move) { if (!state.board?.game) return; const from = move.substring(0, 2), to = move.substring(2, 4); const currentRaw = getRawBoardFEN(); if (currentRaw && sanitizeFEN(currentRaw).split(" ")[0] !== state.lastSentFEN.split(" ")[0]) return; for (const m of state.board.game.getLegalMoves()) { if (m.from === from && m.to === to) { const promotion = move.length > 4 ? move.substring(4, 5) : "q"; state.board.game.move({ ...m, promotion, animate: !0, userGenerated: !0 }); return; } } } function toggleAutoQueue() { if (state.newGameObserver) { state.newGameObserver.disconnect(); state.newGameObserver = null; } if (state.queueTimeout) { clearTimeout(state.queueTimeout); state.queueTimeout = null; } if (settings.autoQueue) { state.newGameObserver = new MutationObserver(() => { const btns = Array.from(document.querySelectorAll("button")); const newGameBtn = btns.find((b) => { const txt = b.innerText.toLowerCase(); return txt.includes("new") && !txt.includes("rematch") && b.offsetParent !== null; }); if (newGameBtn && !state.queueTimeout) { state.queueTimeout = setTimeout(() => { newGameBtn.click(); state.queueTimeout = null; }, 100); } }); state.newGameObserver.observe(document.body, { childList: !0, subtree: !0 }); } } function resetSettings() { const currentModel = settings.engineMode; Object.assign(settings, DEFAULT_SETTINGS); settings.engineMode = currentModel; Object.keys(DEFAULT_SETTINGS).forEach((k) => { if (k !== "engineMode") saveSetting(k, DEFAULT_SETTINGS[k]); }); saveSetting("engineMode", currentModel); const hsl = rgbToHsl(...Object.values(hexToRgb(settings.highlightColor))); state.h = hsl.h; state.s = hsl.s; state.l = hsl.l; toggleAutoQueue(); createUI(); applyMenuPosition(); } function syncColor() { const rgb = hslToRgb(state.h, state.s, state.l); const hex = rgbToHex(rgb.r, rgb.g, rgb.b); settings.highlightColor = hex; saveSetting("highlightColor", hex); if (state.ui.inpR) { state.ui.inpR.value = rgb.r; state.ui.inpG.value = rgb.g; state.ui.inpB.value = rgb.b; state.ui.inpHex.value = hex; state.ui.colorPreview.style.background = hex; state.ui.sliderH.value = state.h; state.ui.sliderS.value = state.s; state.ui.sliderL.value = state.l; if (state.ui.sliderHNum) state.ui.sliderHNum.value = Math.round(state.h); if (state.ui.sliderSNum) state.ui.sliderSNum.value = Math.round(state.s); if (state.ui.sliderLNum) state.ui.sliderLNum.value = Math.round(state.l); } Visuals.removeByType('history'); if (state.currentBestMove) Visuals.add(state.currentBestMove, 'history'); } function applyTheme() { const modals = [state.ui.panel, state.ui.modal, state.ui.histModal, state.ui.localModal]; modals.forEach(m => { if (!m) return; m.style.setProperty("--bot-bg", settings.themeBg); m.style.setProperty("--bot-t", settings.themeText); m.style.setProperty("--bot-b", settings.themeBorder); m.style.setProperty("--bot-p", settings.themePrimary); m.style.color = settings.themeText; if (m === state.ui.panel) { m.style.opacity = settings.menuOpacity; } else { const overlayId = m.id === "modal" ? "modalOv" : m.id === "histModal" ? "histModalOv" : "localModalOv"; const overlay = document.getElementById(overlayId); if (overlay) overlay.style.opacity = "1"; m.style.opacity = settings.menuOpacity; } }); } function applyMenuPosition() { const p = state.ui.panel; if (!p) return; const margin = "10px"; p.style.transform = "none"; p.style.top = ""; p.style.bottom = ""; p.style.left = ""; p.style.right = ""; if (settings.menuPosition === "custom") { const savedX = GM_getValue("bot_pX", "auto"); const savedY = GM_getValue("bot_pY", "0"); if (savedX === "auto") { p.style.right = "0px"; p.style.left = "auto"; } else p.style.left = savedX + "px"; p.style.top = savedY + "px"; const rect = p.getBoundingClientRect(); if (rect.left < 0) p.style.left = "0px"; if (rect.top < 0) p.style.top = "0px"; if (rect.right > window.innerWidth) p.style.left = (window.innerWidth - rect.width) + "px"; if (rect.bottom > window.innerHeight) p.style.top = (window.innerHeight - rect.height) + "px"; } else { switch (settings.menuPosition) { case "top-left": p.style.top = margin; p.style.left = margin; break; case "top-right": p.style.top = margin; p.style.right = margin; break; case "bottom-left": p.style.bottom = margin; p.style.left = margin; break; case "bottom-right": p.style.bottom = margin; p.style.right = margin; break; } } } // Sync all local settings input elements to current settings.localXxx values. // Called when the modal opens or when the model changes. function syncLocalSettingsInputs() { const byId = (id) => document.getElementById(id); const set = (id, val) => { const el = byId(id); if (el) el.value = val; }; const chk = (id, val) => { const el = byId(id); if (el) el.checked = val; }; set("localHashMB", settings.localHashMB); set("localMoveOverhead", settings.localMoveOverhead); set("localSkillLevel", settings.localSkillLevel); set("localSkillLevelRange", settings.localSkillLevel); chk("localLimitStrength", settings.localLimitStrength); set("localElo", settings.localElo); chk("localShowWDL", settings.localShowWDL); set("localMinThinkingTime", settings.localMinThinkTime); set("localSlowMover", settings.localSlowMover); set("localContempt", settings.localContempt); // Show/hide elo row based on limit strength state const eloRow = byId("localEloRow"); if (eloRow) eloRow.style.display = settings.localLimitStrength ? "flex" : "none"; } function updateLocalSettingsUI() { const statusEl = document.getElementById("localEngineStatus"); const statusMsgEl = document.getElementById("localEngineStatusMsg"); const btnInstall = document.getElementById("btnLocalInstall"); const btnReinstall = document.getElementById("btnLocalReinstall"); const btnUninstall = document.getElementById("btnLocalUninstall"); if (!statusEl) return; const m = getEngineById(settings.localModelId || "sf18_05"); // ── Status badge ── const statusMap = { not_installed: { text: "❌ Not Installed", color: "#ff5555" }, loading: { text: "⏳ Loading...", color: "#ffaa00" }, ready: { text: "✅ Ready", color: "#81b64c" }, error: { text: "⚠️ Error", color: "#ff7777" }, }; const s = statusMap[state.engineStatus] || statusMap.not_installed; statusEl.textContent = s.text; statusEl.style.color = s.color; if (statusMsgEl) statusMsgEl.textContent = state.engineStatusMsg; const isLoading = state.engineStatus === "loading"; const isReady = state.engineStatus === "ready"; if (btnInstall) btnInstall.disabled = isReady || isLoading; if (btnReinstall) btnReinstall.disabled = isLoading; if (btnUninstall) btnUninstall.disabled = !isReady && !isLoading; // ── Model caps → show/hide option rows ── const show = (id, visible) => { const el = document.getElementById(id); if (el) el.style.display = visible ? "" : "none"; }; show("rowMoveOverhead", m.hasMoveOverhead); show("rowSkillLevel", m.hasSkillLevel); show("rowLimitStrength", m.hasNNUE); show("rowWDL", m.hasWDL); show("rowSlowMover", m.hasSlowMover); show("rowContempt", m.hasContempt); // ── Model info panel ── const infoEl = document.getElementById("localModelInfo"); if (infoEl) { const fmtLabel = m.format === "asmjs" ? "asm.js — single JS file (no WASM)" : "WASM + JS (unpkg)"; const caps = [ m.hasNNUE ? "NNUE" : "HCE (classical eval)", m.hasSkillLevel ? "Skill Level" : null, m.hasNNUE ? "Elo Limit" : null, m.hasWDL ? "WDL" : null, m.hasContempt ? "Contempt" : null, m.hasSlowMover ? "Slow Mover" : null, m.hasMoveOverhead ? "Move Overhead" : null, ].filter(Boolean).join(" · "); infoEl.innerHTML = `Format: ${fmtLabel}
` + `Max Depth: ${m.maxDepth}
` + `Options: ${caps}
` + `JS: ${m.jsUrl || "(bundled @resource)"}
` + (m.wasmUrl ? `WASM: ${m.wasmUrl}` : "WASM: N/A"); } // ── Source info box ── const srcEl = document.getElementById("localSrcInfo"); if (srcEl) { srcEl.innerHTML = `JS: ${m.jsUrl || "(bundled @resource)"}
` + (m.wasmUrl ? `WASM: ${m.wasmUrl}` : "WASM: N/A — asm.js engine"); } // ── Depth cap ── if (state.ui.inpDepth) state.ui.inpDepth.max = m.maxDepth; if (state.ui.lblMaxDepth) state.ui.lblMaxDepth.innerText = m.maxDepth; } function createUI() { if (document.getElementById("enginePanel")) document.getElementById("enginePanel").remove(); if (document.getElementById("modalOv")) document.getElementById("modalOv").remove(); if (document.getElementById("histModalOv")) document.getElementById("histModalOv").remove(); if (document.getElementById("localModalOv")) document.getElementById("localModalOv").remove(); if (document.getElementById("fenTooltip")) document.getElementById("fenTooltip").remove(); loadSettings(); const initHsl = rgbToHsl(...Object.values(hexToRgb(settings.highlightColor))); state.h = initHsl.h; state.s = initHsl.s; state.l = initHsl.l; const savedW = GM_getValue("bot_panelW", "25vw"); const savedH = GM_getValue("bot_panelH", "50vh"); const isMini = GM_getValue("bot_isMini", false); const S = "#enginePanel"; const SM = "#modal"; const SH = "#histModal"; const SL = "#localModal"; const SO = "#modalOv, #histModalOv, #localModalOv"; // ───────────────────────────────────────────────────────────────────── // IMPROVED CSS — same variables/selectors, cleaner visual system // ───────────────────────────────────────────────────────────────────── const style = ` /* ── CSS custom properties ── */ ${S} { --bot-bg:${settings.themeBg}; --bot-b:${settings.themeBorder}; --bot-p:${settings.themePrimary}; --bot-t:${settings.themeText}; --bot-primary:${settings.themePrimary}; } ${SM} { --bot-bg:${settings.themeBg}; --bot-b:${settings.themeBorder}; --bot-p:${settings.themePrimary}; --bot-t:${settings.themeText}; --bot-primary:${settings.themePrimary}; } ${SH} { --bot-bg:${settings.themeBg}; --bot-b:${settings.themeBorder}; --bot-p:${settings.themePrimary}; --bot-t:${settings.themeText}; --bot-primary:${settings.themePrimary}; } ${SL} { --bot-bg:${settings.themeBg}; --bot-b:${settings.themeBorder}; --bot-p:${settings.themePrimary}; --bot-t:${settings.themeText}; --bot-primary:${settings.themePrimary}; } ${S} *, ${SM} *, ${SH} *, ${SL} * { box-sizing: border-box; } /* ── Main panel ── */ ${S} { position: fixed; width: ${savedW}; height: ${savedH}; min-width: 300px; min-height: 300px; background: var(--bot-bg); border: 1px solid var(--bot-b); color: var(--bot-t); z-index: 9999; font-family: 'Segoe UI', system-ui, sans-serif; font-size: 13px; line-height: 1.4; box-shadow: -2px 0 20px rgba(0,0,0,0.6); display: flex; flex-direction: column; resize: both; overflow: hidden; opacity: ${settings.menuOpacity}; border-radius: 6px; } /* ── Minified state ── */ ${S}.minified { width: 36px !important; height: 36px !important; resize: none; min-height: 0 !important; min-width: 0 !important; overflow: hidden !important; background: var(--bot-p); border: 1px solid var(--bot-b); padding: 0; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer; left: auto !important; top: 0 !important; right: 0 !important; border-radius: 6px; } ${S}.minified #panelContent, ${S}.minified #panelHeader > *:not(#minBtn) { display: none !important; } ${S}.minified #minBtn { width: 100% !important; height: 100% !important; display: flex !important; justify-content: center !important; align-items: center !important; padding: 0 !important; margin: 0 !important; } ${S}.minified #minBtn img { width: 26px !important; height: 26px !important; display: block; } /* ── Panel header ── */ #panelHeader { background: var(--bot-p); color: #000; padding: 0 12px; font-weight: 700; font-size: 12px; letter-spacing: 0.05em; text-transform: uppercase; display: flex; justify-content: space-between; align-items: center; cursor: move; flex: none; user-select: none; height: 36px; flex-shrink: 0; } #panelHeader .header-left { display: flex; align-items: center; gap: 6px; } #minBtn { cursor: pointer; display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; background: rgba(0,0,0,0.15); border-radius: 4px; font-size: 11px; transition: background 0.15s; } #minBtn:hover { background: rgba(0,0,0,0.28); } /* ── Panel content ── */ #panelContent { padding: 12px; display: flex; flex-direction: column; gap: 10px; overflow-y: auto; flex: 1; min-height: 0; } /* ── Sections ── */ ${S} .sect, ${SM} .sect, ${SL} .sect { border-top: 1px solid var(--bot-b); padding-top: 10px; display: flex; flex-direction: column; gap: 7px; } ${S} .sect-title, ${SM} .sect-title, ${SL} .sect-title { font-size: 0.7em; color: #888; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 2px; } /* ── Rows ── */ ${S} .row, ${SM} .row, ${SL} .row { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 2px; } ${S} .row label, ${SM} .row label, ${SL} .row label { font-size: 0.85em; color: var(--bot-t); opacity: 0.85; font-weight: 500; } /* ── Inputs ── */ ${S} input, ${S} select, ${SM} input, ${SM} select, ${SH} input, ${SH} select, ${SL} input, ${SL} select { background: rgba(255,255,255,0.06); color: var(--bot-t); border: 1px solid var(--bot-b); padding: 4px 7px; border-radius: 4px; font-size: 12px; height: 26px; transition: border-color 0.15s; } ${S} select, ${SM} select, ${SH} select, ${SL} select { background-color: #2a2a2a; } ${S} select option, ${SM} select option, ${SH} select option, ${SL} select option { background-color: #2a2a2a; color: #eeeeee; } ${S} input:focus, ${S} select:focus, ${SM} input:focus, ${SM} select:focus, ${SL} input:focus, ${SL} select:focus { outline: none; border-color: var(--bot-p); } ${S} input[type="number"], ${SM} input[type="number"], ${SH} input[type="number"], ${SL} input[type="number"] { width: 60px; text-align: center; } ${S} select, ${SM} select, ${SH} select, ${SL} select { width: 120px; } ${S} input[type="text"], ${SM} input[type="text"], ${SL} input[type="text"] { flex: 1; } /* ── Checkboxes ── */ ${S} input[type="checkbox"], ${SM} input[type="checkbox"], ${SL} input[type="checkbox"] { width: 15px; height: 15px; accent-color: var(--bot-p); cursor: pointer; border: none; background: transparent; } /* ── Range sliders ── */ ${S} input[type=range], ${SM} input[type=range] { -webkit-appearance: none; width: 100%; background: transparent; padding: 0; margin: 0; border: none; height: 18px; } ${S} input[type=range]:focus, ${SM} input[type=range]:focus { outline: none; } ${S} input[type=range]::-webkit-slider-runnable-track, ${SM} input[type=range]::-webkit-slider-runnable-track { width: 100%; height: 4px; cursor: pointer; background: var(--bot-b); border-radius: 2px; } ${S} input[type=range]::-webkit-slider-thumb, ${SM} input[type=range]::-webkit-slider-thumb { height: 14px; width: 14px; border-radius: 50%; background: var(--bot-p); cursor: pointer; -webkit-appearance: none; margin-top: -5px; border: 2px solid rgba(0,0,0,0.2); box-shadow: 0 1px 3px rgba(0,0,0,0.4); transition: transform 0.1s; } ${S} input[type=range]::-webkit-slider-thumb:hover, ${SM} input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.15); } #sliderH { background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00) !important; } #sliderH::-webkit-slider-thumb { background: #fff !important; border: 2px solid #000 !important; } /* ── Buttons ── */ ${S} button, ${SM} button, ${SH} button, ${SL} button { background: var(--bot-p); border: none; padding: 0 12px; height: 30px; color: #000; font-weight: 700; font-size: 12px; cursor: pointer; border-radius: 4px; transition: filter 0.15s, transform 0.1s; letter-spacing: 0.02em; white-space: nowrap; } ${S} button:hover, ${SM} button:hover, ${SH} button:hover, ${SL} button:hover { filter: brightness(1.12); } ${S} button:active, ${SM} button:active { transform: scale(0.97); } ${S} button:disabled, ${SM} button:disabled, ${SL} button:disabled { opacity: 0.45; cursor: not-allowed; filter: none; transform: none; } /* ── Specific named buttons ── */ #btnReset { padding: 0 8px; height: 24px; font-size: 11px; background: rgba(0,0,0,0.18) !important; color: rgba(0,0,0,0.8) !important; border-radius: 3px; } #custBtn { background: #4fc3f7 !important; color: #000 !important; } #histBtn { background: #b39ddb !important; color: #000 !important; } #localBtn { background: #ffcc80 !important; color: #000 !important; } #btnAnalyze { width: 100%; height: 34px; font-size: 13px; letter-spacing: 0.04em; } #custBtn, #histBtn, #localBtn { width: 100%; } /* ── Utility button row ── */ .btn-row { display: flex; flex-direction: column; gap: 6px; } /* ── Log boxes ── */ .log-box { background: rgba(0,0,0,0.4); padding: 7px 9px; font-family: 'Cascadia Code', 'Fira Mono', monospace; font-size: 0.72em; border-radius: 4px; overflow-y: auto; word-break: break-all; white-space: pre-wrap; border: 1px solid var(--bot-b); height: 90px; resize: vertical; user-select: text !important; -webkit-user-select: text !important; cursor: text; color: #ccc; } /* ── Status box ── */ #statusBox { background: rgba(0,0,0,0.18); padding: 8px 10px; border: 1px solid rgba(0,188,212,0.35); border-radius: 5px; font-size: 0.88em; min-height: 42px; width: 100%; flex-shrink: 0; display: flex; flex-direction: column; gap: 4px; } /* ── Move result ── */ #moveResult { background: rgba(0,0,0,0.18); padding: 5px 10px; border-radius: 4px; text-align: center; font-size: 0.88em; border: 1px solid var(--bot-b); } /* ── Delay display ── */ #delayDisplay { font-size: 0.7em; color: #666; text-align: right; margin-top: -4px; } /* ── Overlays ── */ ${SO} { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.75); z-index: 10000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(2px); } /* ── Modals ── */ ${SM}, ${SH}, ${SL} { background: var(--bot-bg); padding: 0; border-radius: 8px; width: 480px; border: 1px solid var(--bot-b); display: flex; flex-direction: column; max-height: 90vh; opacity: ${settings.menuOpacity}; box-shadow: 0 8px 32px rgba(0,0,0,0.5); } ${SH} { width: 600px; height: 600px; } ${SM} *, ${SL} * { color: var(--bot-t); } ${SM} label, ${SH} label, ${SL} label { opacity: 1 !important; font-weight: 600; font-size: 0.88em; } ${SM} input[type="color"], ${SH} input[type="color"] { height: 26px; padding: 0; width: 40px; cursor: pointer; border: none; } ${SM} select, ${SH} select, ${SL} select { height: 26px; padding: 0 6px; font-size: 0.88em; } /* ── Mode-dependent cloud rows ── */ ${S} .show-cloud { display: none; } ${S} .show-local { display: none; } body.mode-cloud ${S} .show-cloud { display: flex; } body.mode-local ${S} .show-local { display: flex; } /* ── RGB inputs ── */ ${SM} .rgb-inputs, ${S} .rgb-inputs { display: flex; gap: 5px; flex: 1; justify-content: flex-end; } ${SM} .rgb-inputs input, ${S} .rgb-inputs input { width: 46px; text-align: center; } /* ── History table ── */ #histTableContainer { flex: 1; overflow-y: auto; border: 1px solid #444; border-radius: 4px; margin-top: 10px; } #histTable { width: 100%; border-collapse: collapse; font-size: 0.83em; } #histTable th { background: var(--bot-b); color: var(--bot-p); position: sticky; top: 0; z-index: 1; padding: 8px 6px; font-size: 0.8em; letter-spacing: 0.05em; text-transform: uppercase; } #histTable td { border-bottom: 1px solid rgba(255,255,255,0.05); padding: 7px 6px; text-align: left; color: var(--bot-t); } #histTable tr:hover td { background: rgba(255,255,255,0.04); } .hist-win { color: #81b64c; font-weight: 700; } .hist-loss { color: #ff5555; font-weight: 700; } .hist-draw { color: #888; font-weight: 700; } .hist-fen { max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; color: #666; text-decoration: underline dotted; } .btn-del { background: #c0392b !important; color: white !important; padding: 2px 8px; border-radius: 3px; font-size: 0.72em; cursor: pointer; border: none; height: auto; } .hist-controls { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; } #histEmpty { padding: 24px; text-align: center; color: #555; font-style: italic; } /* ── FEN tooltip ── */ #fenTooltip { position: fixed; border: 2px solid #444; background: #1a1a1a; z-index: 10001; display: none; pointer-events: none; box-shadow: 0 4px 20px rgba(0,0,0,0.6); border-radius: 5px; overflow: hidden; } .fen-board { display: grid; grid-template-columns: repeat(8, 1fr); width: 240px; height: 240px; } .fen-sq { width: 30px; height: 30px; display: flex; justify-content: center; align-items: center; background-size: 100%; background-repeat: no-repeat; } .fen-sq.light { background-color: #eeeed2; } .fen-sq.dark { background-color: #769656; } /* ── Modal header ── */ .modal-header { display: flex; justify-content: space-between; align-items: center; padding: 14px 16px; border-bottom: 1px solid var(--bot-b); } .modal-header h3 { margin: 0; font-size: 14px; font-weight: 700; letter-spacing: 0.03em; } .modal-header button { padding: 0 !important; width: 26px; height: 26px; background: rgba(255,255,255,0.08) !important; color: var(--bot-t) !important; border-radius: 4px; font-size: 16px; line-height: 1; } .modal-header button:hover { background: rgba(255,255,255,0.16) !important; } /* ── Modal tabs ── */ .modal-tabs { display: flex; border-bottom: 1px solid var(--bot-b); } ${SM} .tab-btn { flex: 1; background: transparent !important; border: none !important; border-bottom: 2px solid transparent !important; padding: 10px; color: var(--bot-t) !important; cursor: pointer; opacity: 0.55; font-size: 12px; font-weight: 600; letter-spacing: 0.03em; transition: opacity 0.15s; height: auto; } ${SM} .tab-btn:hover { opacity: 0.85; } ${SM} .tab-btn.active { opacity: 1; border-bottom: 2px solid var(--bot-p) !important; } /* ── Modal content ── */ .modal-content { padding: 14px 16px; overflow-y: auto; flex: 1; } ${SM} .modal-content .row { display: flex; align-items: center; margin-bottom: 11px; } ${SM} .modal-content .row label { flex: 0 0 128px; text-align: left; font-weight: 600; } ${SM} .modal-content .row > input[type="text"], ${SM} .modal-content .row > input[type="color"], ${SM} .modal-content .row > select { flex: 1; } /* ── Slider groups ── */ ${S} .slider-group, ${SM} .slider-group { display: flex; align-items: center; gap: 7px; flex: 1; justify-content: flex-end; } ${S} .slider-group input[type=range], ${SM} .slider-group input[type=range] { flex: 1; } ${S} .slider-group input[type=number], ${SM} .slider-group input[type=number] { width: 46px; text-align: center; } ${S} .slider-group span, ${SM} .slider-group span { font-size: 0.78em; color: #777; min-width: 14px; } /* ── Advanced toggles ── */ .adv-toggle { cursor: pointer; font-size: 0.78em; color: var(--bot-p); text-decoration: none; margin-top: 4px; display: inline-flex; align-items: center; gap: 4px; opacity: 0.85; transition: opacity 0.15s; } .adv-toggle:hover { opacity: 1; } .adv-sect { margin-top: 8px; padding-left: 10px; border-left: 2px solid var(--bot-b); display: flex; flex-direction: column; gap: 7px; } /* ── Theme presets ── */ .theme-presets { display: flex; gap: 8px; margin-bottom: 12px; } ${SM} .theme-btn { flex: 1; padding: 0 !important; height: 30px !important; border: 1px solid var(--bot-b) !important; background: rgba(255,255,255,0.05) !important; color: var(--bot-t) !important; font-size: 12px !important; } /* ── Local modal specifics ── */ #localEngineStatus { font-weight: 700; font-size: 1em; } #localEngineStatusMsg { font-size: 0.78em; color: #888; margin-top: 3px; min-height: 14px; } .local-action-btn { padding: 0 14px !important; font-size: 0.83em !important; height: 30px !important; } .local-btn-install { background: #27ae60 !important; color: #fff !important; } .local-btn-reinstall { background: #2980b9 !important; color: #fff !important; } .local-btn-uninstall { background: #c0392b !important; color: #fff !important; } ${SL} .info-box { background: rgba(0,0,0,0.25); border: 1px solid var(--bot-b); border-radius: 4px; padding: 7px 10px; font-size: 0.78em; font-family: 'Cascadia Code', 'Fira Mono', monospace; color: #999; word-break: break-all; } ${SL} input[type="text"] { width: 100%; font-size: 0.83em; } ${SL} select { width: 100%; } /* ── Automation checkboxes layout ── */ .auto-checks { display: flex; gap: 14px; flex-wrap: wrap; } .auto-checks label { display: flex; align-items: center; gap: 5px; font-size: 0.83em; cursor: pointer; white-space: nowrap; } /* ── PV section header ── */ .pv-header { display: flex; justify-content: space-between; align-items: center; width: 100%; } `; const fullHTML = `` + `
SF Engine ${isMini ? `` : "▼"}
${state.lastLiveResult}
${state.lastMoveResult}
Engine
PV Arrows
Automation
to
Next: N/A
DateColorResultClockFEN
`; document.body.insertAdjacentHTML("beforeend", fullHTML); const panel = document.getElementById("enginePanel"); const computed = window.getComputedStyle(panel); panel.style.width = computed.width; if (!isMini) panel.style.height = computed.height; state.ui = { panel: panel, header: document.getElementById("panelHeader"), minBtn: document.getElementById("minBtn"), moveResult: document.getElementById("moveResult"), liveOutput: document.getElementById("statusBox"), logSent: document.getElementById("sentCommandOutput"), logRec: document.getElementById("receivedMessageOutput"), delayDisplay: document.getElementById("delayDisplay"), btnAnalyze: document.getElementById("btnAnalyze"), selMode: document.getElementById("selMode"), inpDepth: document.getElementById("inpDepth"), inpTime: document.getElementById("inpTime"), inpSearch: document.getElementById("inpSearch"), chkRun: document.getElementById("chkRun"), chkMove: document.getElementById("chkMove"), chkQueue: document.getElementById("chkQueue"), rowMoveKeybind: document.getElementById("rowMoveKeybind"), btnMoveKeybind: document.getElementById("btnMoveKeybind"), chkHideAfterMove: document.getElementById("chkHideAfterMove"), chkPV: document.getElementById("chkPV"), inpPVDepth: document.getElementById("inpPVDepth"), inpPVDepthNum: document.getElementById("inpPVDepthNum"), chkPVNums: document.getElementById("chkPVNums"), chkPVGrad: document.getElementById("chkPVGrad"), inpPVStart: document.getElementById("inpPVStart"), inpPVEnd: document.getElementById("inpPVEnd"), pvSettings: document.getElementById("pvSettings"), pvGradSettings: document.getElementById("pvGradSettings"), inpMin: document.getElementById("inpMin"), inpMax: document.getElementById("inpMax"), chkDebug: document.getElementById("chkDebug"), debugArea: document.getElementById("debugArea"), btnReset: document.getElementById("btnReset"), lblMaxDepth: document.getElementById("lblMaxDepth"), custBtn: document.getElementById("custBtn"), histBtn: document.getElementById("histBtn"), localBtn: document.getElementById("localBtn"), modal: document.getElementById("modalOv"), modalClose: document.getElementById("modalClose"), histModal: document.getElementById("histModalOv"), histModalClose: document.getElementById("histModalClose"), histBody: document.getElementById("histBody"), btnClearHist: document.getElementById("btnClearHist"), chkHistory: document.getElementById("chkHistory"), localModal: document.getElementById("localModalOv"), localModalClose: document.getElementById("localModalClose"), visType: document.getElementById("visType"), visBoxSettings: document.getElementById("visBoxSettings"), visArrowSettings: document.getElementById("visArrowSettings"), visOutlineSettings: document.getElementById("visOutlineSettings"), sliderH: document.getElementById("sliderH"), sliderHNum: document.getElementById("sliderHNum"), sliderS: document.getElementById("sliderS"), sliderSNum: document.getElementById("sliderSNum"), sliderL: document.getElementById("sliderL"), sliderLNum: document.getElementById("sliderLNum"), colorPreview: document.getElementById("colorPreview"), inpR: document.getElementById("inpR"), inpG: document.getElementById("inpG"), inpB: document.getElementById("inpB"), inpHex: document.getElementById("inpHex"), fenTooltip: document.getElementById("fenTooltip"), tabMove: document.getElementById("tabMove"), tabTheme: document.getElementById("tabTheme"), tabContentMove: document.getElementById("tabContentMove"), tabContentTheme: document.getElementById("tabContentTheme"), advToggle: document.getElementById("advToggle"), advSect: document.getElementById("advSect"), visInnerOp: document.getElementById("visInnerOp"), visInnerOpNum: document.getElementById("visInnerOpNum"), visOuterOp: document.getElementById("visOuterOp"), visOuterOpNum: document.getElementById("visOuterOpNum"), visBias: document.getElementById("visBias"), visBiasNum: document.getElementById("visBiasNum"), visArrowOp: document.getElementById("visArrowOp"), visArrowOpNum: document.getElementById("visArrowOpNum"), visArrowWidth: document.getElementById("visArrowWidth"), visArrowWidthNum: document.getElementById("visArrowWidthNum"), visOutOp: document.getElementById("visOutOp"), visOutOpNum: document.getElementById("visOutOpNum"), visOutWidth: document.getElementById("visOutWidth"), visOutWidthNum: document.getElementById("visOutWidthNum"), visOutGlow: document.getElementById("visOutGlow"), visOutGlowRad: document.getElementById("visOutGlowRad"), visOutGlowRadNum: document.getElementById("visOutGlowRadNum"), btnThemeDark: document.getElementById("btnThemeDark"), btnThemeLight: document.getElementById("btnThemeLight"), inpMenuOp: document.getElementById("inpMenuOp"), inpMenuOpNum: document.getElementById("inpMenuOpNum"), colBg: document.getElementById("colBg"), colTxt: document.getElementById("colTxt"), colBorder: document.getElementById("colBorder"), colPrim: document.getElementById("colPrim"), selMenuPos: document.getElementById("selMenuPos") }; applyMenuPosition(); // Init engine status — if already loaded keep it, otherwise auto-load if in local mode if (state.localEngine) { setEngineStatus("ready", ""); } else { updateLocalSettingsUI(); if (settings.engineMode === "local" && !state.engineLoadingInProgress) { loadLocalEngine(); } } // Bindings (all identical to original) state.ui.selMode.value = settings.engineMode; state.ui.selMenuPos.value = settings.menuPosition; state.ui.btnAnalyze.onclick = () => analyze(); state.ui.btnReset.onclick = resetSettings; state.ui.custBtn.onclick = () => (state.ui.modal.style.display = "flex"); state.ui.modalClose.onclick = () => (state.ui.modal.style.display = "none"); state.ui.histBtn.onclick = () => { renderHistory(); state.ui.histModal.style.display = "flex"; }; state.ui.histModalClose.onclick = () => (state.ui.histModal.style.display = "none"); state.ui.btnClearHist.onclick = () => { if (confirm("Delete all history?")) { state.history = []; GM_setValue("bot_history", []); renderHistory(); } }; state.ui.localBtn.onclick = () => { loadModelSettings(settings.localModelId); syncLocalSettingsInputs(); updateLocalSettingsUI(); state.ui.localModal.style.display = "flex"; }; state.ui.localModalClose.onclick = () => (state.ui.localModal.style.display = "none"); document.getElementById("btnLocalInstall").onclick = () => { loadLocalEngine(); updateLocalSettingsUI(); }; document.getElementById("btnLocalReinstall").onclick = () => reinstallEngine(); document.getElementById("btnLocalUninstall").onclick = () => { if (confirm("Uninstall local engine and clear cache?")) uninstallEngine(); }; // ── Model selector ───────────────────────────────────────────────── const localModelSel = document.getElementById("localModelSel"); localModelSel.onchange = (e) => { const newId = e.target.value; // Shut down any currently running engine if (state.localEngine) { try { state.localEngine.terminate(); } catch(_) {} state.localEngine = null; } state.engineLoadingInProgress = false; saveSetting("localModelId", newId); // Load this model's saved per-model settings into working state loadModelSettings(newId); // Clamp depth to new model's cap const m = getEngineById(newId); if (settings.depth > m.maxDepth) { settings.depth = m.maxDepth; saveSetting("depth", m.maxDepth); if (state.ui.inpDepth) state.ui.inpDepth.value = m.maxDepth; } // Refresh all input values in the modal to show this model's settings syncLocalSettingsInputs(); setEngineStatus("not_installed", ""); updateLocalSettingsUI(); loadLocalEngine(); }; // Populate model info immediately on open syncLocalSettingsInputs(); updateLocalSettingsUI(); // ── Shared helpers ───────────────────────────────────────────────── const sendOpt = (name, val) => { if (state.localEngine) state.localEngine.postMessage(`setoption name ${name} value ${val}`); }; // Save a setting scoped to the currently selected model const ms = (key, val) => saveModelSetting(key, val); // ── Hash ─────────────────────────────────────────────────────────── document.getElementById("localHashMB").oninput = (e) => { const v = parseInt(e.target.value) || 64; ms("localHashMB", v); sendOpt("Hash", v); }; // ── Move Overhead ────────────────────────────────────────────────── document.getElementById("localMoveOverhead").oninput = (e) => { const v = parseInt(e.target.value) || 100; ms("localMoveOverhead", v); sendOpt("Move Overhead", v); }; // ── Skill Level (range + number synced) ─────────────────────────── const skillNum = document.getElementById("localSkillLevel"); const skillRange = document.getElementById("localSkillLevelRange"); const applySkill = (v) => { ms("localSkillLevel", v); sendOpt("Skill Level", v); }; skillNum.oninput = (e) => { const v = Math.min(20, Math.max(0, parseInt(e.target.value)||0)); skillRange.value = v; applySkill(v); }; skillRange.oninput = (e) => { skillNum.value = e.target.value; applySkill(parseInt(e.target.value)); }; // ── Elo Limit ────────────────────────────────────────────────────── const limitChk = document.getElementById("localLimitStrength"); const eloRow = document.getElementById("localEloRow"); const eloInp = document.getElementById("localElo"); limitChk.onchange = (e) => { ms("localLimitStrength", e.target.checked); eloRow.style.display = e.target.checked ? "flex" : "none"; sendOpt("UCI_LimitStrength", e.target.checked); }; eloInp.oninput = (e) => { const v = Math.min(3190, Math.max(1320, parseInt(e.target.value)||1320)); ms("localElo", v); sendOpt("UCI_Elo", v); }; // ── Advanced toggle ──────────────────────────────────────────────── const localAdvToggle = document.getElementById("localAdvToggle"); const localAdvSect = document.getElementById("localAdvSect"); localAdvToggle.onclick = () => { const open = localAdvSect.style.display === "none" || localAdvSect.style.display === ""; localAdvSect.style.display = open ? "flex" : "none"; localAdvToggle.innerText = open ? "▲ Advanced Options" : "▼ Advanced Options"; }; // ── WDL ──────────────────────────────────────────────────────────── document.getElementById("localShowWDL").onchange = (e) => { ms("localShowWDL", e.target.checked); sendOpt("UCI_ShowWDL", e.target.checked); }; // ── Min Thinking Time ────────────────────────────────────────────── document.getElementById("localMinThinkingTime").oninput = (e) => { const v = parseInt(e.target.value) || 20; ms("localMinThinkTime", v); sendOpt("Minimum Thinking Time", v); }; // ── Slow Mover ───────────────────────────────────────────────────── document.getElementById("localSlowMover").oninput = (e) => { const v = Math.min(1000, Math.max(10, parseInt(e.target.value)||100)); ms("localSlowMover", v); sendOpt("Slow Mover", v); }; // ── Contempt ──────────────────────────────────────────────────────── { const contemptInp = document.getElementById("localContempt"); if (contemptInp) contemptInp.oninput = (e) => { const v = Math.min(100, Math.max(-100, parseInt(e.target.value) || 0)); ms("localContempt", v); sendOpt("Contempt", v); }; } const toggleMin = () => { const isMini = state.ui.panel.classList.toggle("minified"); saveSetting("isMini", isMini); state.ui.minBtn.innerHTML = isMini ? `` : "▼"; }; state.ui.minBtn.onclick = (e) => { e.stopPropagation(); toggleMin(); }; state.ui.panel.onclick = (e) => { if (state.ui.panel.classList.contains("minified")) toggleMin(); }; if (isMini) state.ui.minBtn.innerHTML = ``; const bind = (el, key, type = "val") => { if (!el) return; el.addEventListener(type === "chk" ? "change" : "input", (e) => { const val = type === "chk" ? e.target.checked : type === "num" ? parseFloat(e.target.value) : e.target.value; saveSetting(key, val); if (key === "autoMove" && val === !0) triggerAutoMove(); if (key === "autoQueue") toggleAutoQueue(); if (key === "hideAfterMove" && val === !0) { Visuals.removeByType('history'); Visuals.removeByType('analysis'); PV.clear(); } if (["innerOpacity","outerOpacity","gradientBias","arrowOpacity","arrowWidth","visualOutlineWidth","visualOutlineOpacity","visualOutlineGlow","visualOutlineGlowRadius"].includes(key) && state.currentBestMove) { Visuals.removeByType('history'); Visuals.add(state.currentBestMove, 'history'); } if (["themeBg","themeText","themeBorder","themePrimary","menuOpacity"].includes(key)) applyTheme(); updateUI(); }); }; const bindSlider = (rangeEl, numEl, key, isPct = false) => { if (!rangeEl || !numEl) return; rangeEl.oninput = () => { let val = parseFloat(rangeEl.value); saveSetting(key, val); numEl.value = isPct ? Math.round(val * 100) : val; if (key === "menuOpacity") applyTheme(); if (state.currentBestMove) { Visuals.removeByType('history'); Visuals.add(state.currentBestMove, 'history'); } }; numEl.oninput = () => { let val = parseFloat(numEl.value); if (isPct) val /= 100; saveSetting(key, val); rangeEl.value = val; if (key === "menuOpacity") applyTheme(); if (state.currentBestMove) { Visuals.removeByType('history'); Visuals.add(state.currentBestMove, 'history'); } }; }; state.ui.selMenuPos.onchange = (e) => { saveSetting("menuPosition", e.target.value); applyMenuPosition(); }; state.ui.header.onmousedown = (e) => { if (e.target.id === "minBtn" || e.target.id === "btnReset") return; if (state.ui.panel.classList.contains("minified")) return; if (settings.menuPosition !== 'custom') { saveSetting("menuPosition", 'custom'); state.ui.selMenuPos.value = 'custom'; } e.preventDefault(); const startX = e.clientX - state.ui.panel.offsetLeft; const startY = e.clientY - state.ui.panel.offsetTop; const onMove = (mv) => { let x = mv.clientX - startX, y = mv.clientY - startY; x = Math.max(0, Math.min(x, window.innerWidth - state.ui.panel.offsetWidth)); y = Math.max(0, Math.min(y, window.innerHeight - state.ui.panel.offsetHeight)); state.ui.panel.style.left = x + "px"; state.ui.panel.style.top = y + "px"; state.ui.panel.style.right = "auto"; state.ui.panel.style.bottom = "auto"; saveSetting("pX", x); saveSetting("pY", y); }; document.addEventListener("mousemove", onMove); document.onmouseup = () => document.removeEventListener("mousemove", onMove); }; new ResizeObserver(() => { if (!state.ui.panel.classList.contains("minified")) { saveSetting("panelW", state.ui.panel.style.width); saveSetting("panelH", state.ui.panel.style.height); } }).observe(state.ui.panel); state.ui.selMode.onchange = (e) => { saveSetting("engineMode", e.target.value); state.isThinking = false; if (settings.engineMode === "local") { // Auto-load immediately when switching to local if (!state.localEngine && !state.engineLoadingInProgress) { loadLocalEngine(); } } else { // Switching away from local — kill thinking but keep engine warm if (state.currentCloudRequest) { try { state.currentCloudRequest.abort(); } catch(_) {} state.currentCloudRequest = null; } } updateUI(); }; state.ui.chkDebug.onchange = (e) => { saveSetting("debugLogs", e.target.checked); updateUI(); }; const durSlider = document.getElementById("visDuration"); const durText = document.getElementById("visDurationText"); const rowFade = document.getElementById("rowFadeOut"); const chkFade = document.getElementById("chkFadeOut"); const sliderToSeconds = (val) => { if (val <= 0) return -1; if (val >= 100) return 0; return Math.round((59.9 * Math.pow((val-1)/98,2)+0.1)*10)/10; }; const secondsToSlider = (secs) => { if (secs === -1) return 0; if (secs === 0) return 100; return Math.round(Math.sqrt((secs-0.1)/59.9)*98)+1; }; durSlider.value = secondsToSlider(settings.visualDuration); chkFade.checked = settings.visualFadeOut; const updateDurUI = () => { const val = parseInt(durSlider.value); const isNative = settings.visualType === "nativeArrow"; if (val >= 100) { durText.innerText = "Forever"; if(rowFade) rowFade.style.display = "none"; saveSetting("visualDuration", 0); } else if (val <= 0) { durText.innerText = "Disabled"; if(rowFade) rowFade.style.display = "none"; saveSetting("visualDuration", -1); } else { const secs = sliderToSeconds(val); durText.innerText = secs.toFixed(1) + "s"; if(rowFade) rowFade.style.display = isNative ? "none" : "flex"; saveSetting("visualDuration", secs); } }; durSlider.oninput = updateDurUI; chkFade.onchange = (e) => saveSetting("visualFadeOut", e.target.checked); updateDurUI(); state.ui.visType.onchange = (e) => { saveSetting("visualType", e.target.value); toggleVisualInputs(); Visuals.removeByType('history'); if (state.currentBestMove) Visuals.add(state.currentBestMove, 'history'); }; function toggleVisualInputs() { state.ui.visBoxSettings.style.display = "none"; state.ui.visArrowSettings.style.display = "none"; state.ui.visOutlineSettings.style.display = "none"; const isNative = settings.visualType === "nativeArrow"; const rowDur = document.getElementById("rowDuration"); const sectColor = document.getElementById("sectHighlightColor"); const sectAdv = document.getElementById("sectAdvancedVis"); if (rowDur) rowDur.style.display = isNative ? "none" : "flex"; if (sectColor) sectColor.style.display = isNative ? "none" : "flex"; if (sectAdv) sectAdv.style.display = isNative ? "none" : "block"; updateDurUI(); // Update fade out visibility based on the new logic if (!isNative) { if (settings.visualType === "arrow") state.ui.visArrowSettings.style.display = "block"; else if (settings.visualType === "outline") state.ui.visOutlineSettings.style.display = "block"; else state.ui.visBoxSettings.style.display = "block"; } } state.ui.visType.value = settings.visualType; toggleVisualInputs(); state.ui.tabMove.onclick = () => { state.ui.tabMove.classList.add("active"); state.ui.tabTheme.classList.remove("active"); state.ui.tabContentMove.style.display = "block"; state.ui.tabContentTheme.style.display = "none"; }; state.ui.tabTheme.onclick = () => { state.ui.tabTheme.classList.add("active"); state.ui.tabMove.classList.remove("active"); state.ui.tabContentTheme.style.display = "block"; state.ui.tabContentMove.style.display = "none"; }; state.ui.advToggle.onclick = () => { const isH = state.ui.advSect.style.display==="none"; state.ui.advSect.style.display = isH?"block":"none"; state.ui.advToggle.innerText = isH?"▲ Advanced Visual Settings":"▼ Advanced Visual Settings"; }; state.ui.btnThemeDark.onclick = () => { state.ui.colBg.value="#222222"; state.ui.colTxt.value="#eeeeee"; state.ui.colBorder.value="#444444"; state.ui.colPrim.value="#81b64c"; ["themeBg","themeText","themeBorder","themePrimary"].forEach(k => saveSetting(k, k==="themeBg"?"#222222":k==="themeText"?"#eeeeee":k==="themeBorder"?"#444444":"#81b64c")); applyTheme(); }; state.ui.btnThemeLight.onclick = () => { state.ui.colBg.value="#f0f0f0"; state.ui.colTxt.value="#222222"; state.ui.colBorder.value="#cccccc"; state.ui.colPrim.value="#81b64c"; ["themeBg","themeText","themeBorder","themePrimary"].forEach(k => saveSetting(k, k==="themeBg"?"#f0f0f0":k==="themeText"?"#222222":k==="themeBorder"?"#cccccc":"#81b64c")); applyTheme(); }; bind(state.ui.inpDepth, "depth", "num"); bind(state.ui.inpTime, "maxThinkingTime", "num"); bind(state.ui.inpSearch, "searchMoves"); bind(state.ui.chkRun, "autoRun", "chk"); bind(state.ui.chkMove, "autoMove", "chk"); bind(state.ui.chkQueue, "autoQueue", "chk"); // Toggle the visibility of the Keybind row when Auto-Move is checked/unchecked state.ui.chkMove.addEventListener("change", (e) => { state.ui.rowMoveKeybind.style.display = e.target.checked ? "none" : "flex"; }); // Handle recording the user's keystroke if (state.ui.btnMoveKeybind) { state.ui.btnMoveKeybind.onclick = (e) => { e.stopPropagation(); state.ui.btnMoveKeybind.innerText = "Press key..."; const keyHandler = (ev) => { ev.preventDefault(); ev.stopPropagation(); document.removeEventListener("keydown", keyHandler, true); let keyName = ev.key; if (keyName === " ") keyName = "Space"; // Format spacebar nicely if (keyName === "Escape") { settings.moveKeybind = "None"; } else { settings.moveKeybind = keyName; } saveSetting("moveKeybind", settings.moveKeybind); state.ui.btnMoveKeybind.innerText = settings.moveKeybind; }; // Listen for the very next keydown event document.addEventListener("keydown", keyHandler, true); }; } bind(state.ui.chkHideAfterMove, "hideAfterMove", "chk"); bind(state.ui.chkPV, "showPVArrows", "chk"); bindSlider(state.ui.inpPVDepth, state.ui.inpPVDepthNum, "pvDepth", false); bind(state.ui.chkPVNums, "pvShowNumbers", "chk"); bind(state.ui.chkPVGrad, "pvCustomGradient", "chk"); bind(state.ui.inpPVStart, "pvStartColor"); bind(state.ui.inpPVEnd, "pvEndColor"); bind(state.ui.inpMin, "minDelay", "num"); bind(state.ui.inpMax, "maxDelay", "num"); bindSlider(state.ui.visInnerOp, state.ui.visInnerOpNum, "innerOpacity", true); bindSlider(state.ui.visOuterOp, state.ui.visOuterOpNum, "outerOpacity", true); bindSlider(state.ui.visBias, state.ui.visBiasNum, "gradientBias", false); bindSlider(state.ui.visArrowOp, state.ui.visArrowOpNum, "arrowOpacity", true); bindSlider(state.ui.visArrowWidth, state.ui.visArrowWidthNum, "arrowWidth", false); bindSlider(state.ui.visOutOp, state.ui.visOutOpNum, "visualOutlineOpacity", true); bindSlider(state.ui.visOutWidth, state.ui.visOutWidthNum, "visualOutlineWidth", false); bind(state.ui.visOutGlow, "visualOutlineGlow", "chk"); bindSlider(state.ui.visOutGlowRad, state.ui.visOutGlowRadNum, "visualOutlineGlowRadius", false); bindSlider(state.ui.inpMenuOp, state.ui.inpMenuOpNum, "menuOpacity", true); bind(state.ui.colBg, "themeBg"); bind(state.ui.colTxt, "themeText"); bind(state.ui.colBorder, "themeBorder"); bind(state.ui.colPrim, "themePrimary"); [state.ui.sliderH, state.ui.sliderS, state.ui.sliderL].forEach(el => { el.oninput = () => { state.h = parseFloat(state.ui.sliderH.value); state.s = parseFloat(state.ui.sliderS.value); state.l = parseFloat(state.ui.sliderL.value); syncColor(); }; }); if (state.ui.sliderHNum) state.ui.sliderHNum.oninput = (e) => { state.h = Math.min(360, Math.max(0, parseFloat(e.target.value) || 0)); state.ui.sliderH.value = state.h; syncColor(); }; if (state.ui.sliderSNum) state.ui.sliderSNum.oninput = (e) => { state.s = Math.min(100, Math.max(0, parseFloat(e.target.value) || 0)); state.ui.sliderS.value = state.s; syncColor(); }; if (state.ui.sliderLNum) state.ui.sliderLNum.oninput = (e) => { state.l = Math.min(100, Math.max(0, parseFloat(e.target.value) || 0)); state.ui.sliderL.value = state.l; syncColor(); }; const applyRgb = () => { const r = Math.min(255, Math.max(0, parseInt(state.ui.inpR.value) || 0)); const g = Math.min(255, Math.max(0, parseInt(state.ui.inpG.value) || 0)); const b = Math.min(255, Math.max(0, parseInt(state.ui.inpB.value) || 0)); const hsl = rgbToHsl(r, g, b); state.h = hsl.h; state.s = hsl.s; state.l = hsl.l; syncColor(); }; if (state.ui.inpR) state.ui.inpR.oninput = applyRgb; if (state.ui.inpG) state.ui.inpG.oninput = applyRgb; if (state.ui.inpB) state.ui.inpB.oninput = applyRgb; state.ui.inpHex.oninput = (e) => { if (/^#?[0-9A-F]{6}$/i.test(e.target.value)) { const hex = e.target.value.startsWith("#") ? e.target.value : "#" + e.target.value; const rgb = hexToRgb(hex); const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b); state.h = hsl.h; state.s = hsl.s; state.l = hsl.l; syncColor(); } }; } function drawFenBoard(fen) { let rows = fen.split(" ")[0].split("/"), board = []; for (let r of rows) { let rowArr = []; for (let char of r) { if (!isNaN(char)) { for (let k = 0; k < parseInt(char); k++) rowArr.push(""); } else rowArr.push(char); } board.push(rowArr); } let html = '
'; for (let r = 0; r < 8; r++) for (let c = 0; c < 8; c++) { const piece = board[r][c], isDark = (r+c)%2===1; const bg = piece ? `style="background-image: url('${PIECE_IMGS[piece]}');"` : ""; html += `
`; } return html + "
"; } function renderHistory() { if (!state.ui.histBody) return; state.ui.histBody.innerHTML = ""; if (state.history.length === 0) { state.ui.histBody.innerHTML = 'No history yet.'; return; } [...state.history].reverse().forEach((item, index) => { const tr = document.createElement("tr"); let resClass = item.result === "Win" ? "hist-win" : item.result === "Loss" ? "hist-loss" : "hist-draw"; tr.innerHTML = ` ${item.date} ${item.color || "N/A"} ${item.result} ${item.myTime} / ${item.oppTime} ${item.fen} `; state.ui.histBody.appendChild(tr); }); document.querySelectorAll(".btn-del").forEach((btn) => { btn.onclick = (e) => { const idx = parseInt(e.target.dataset.idx); state.history.splice(idx, 1); GM_setValue("bot_history", state.history); renderHistory(); }; }); document.querySelectorAll(".hist-fen").forEach((el) => { el.onmouseenter = (e) => { const fen = e.target.getAttribute("data-fen"); if (fen && state.ui.fenTooltip) { state.ui.fenTooltip.innerHTML = drawFenBoard(fen); state.ui.fenTooltip.style.display = "block"; const rect = e.target.getBoundingClientRect(); let left = rect.left + 20, top = rect.bottom + 5; if (left + 250 > window.innerWidth) left = window.innerWidth - 260; if (top + 250 > window.innerHeight) top = rect.top - 260; state.ui.fenTooltip.style.left = left + "px"; state.ui.fenTooltip.style.top = top + "px"; } }; el.onmouseleave = () => { if (state.ui.fenTooltip) state.ui.fenTooltip.style.display = "none"; }; }); } function checkForGameOver() { if (!settings.enableHistory) return; const resultEl = document.querySelector(".game-result-component, .game-over-modal-content, .daily-game-footer-game-over"); if (resultEl) { if (state.hasSavedCurrentGameResult) return; let fen = sanitizeFEN(getRawBoardFEN()); let playingAsCode = state.playingAs; if (!playingAsCode && state.board?.game?.getPlayingAs) { try { playingAsCode = state.board.game.getPlayingAs(); } catch (e) {} } if (playingAsCode !== 1 && playingAsCode !== 2) playingAsCode = 0; const playerColor = playingAsCode === 2 ? "Black" : "White"; if (playingAsCode === 2) { let parts = fen.split(" "); if (parts.length > 0) { parts[0] = parts[0].split("/").reverse().map((row) => row.split("").reverse().join("")).join("/"); fen = parts.join(" "); } } const clockBot = document.querySelector(".clock-bottom .clock-time-monospace, .clock-bottom"); const clockTop = document.querySelector(".clock-top .clock-time-monospace, .clock-top"); let myTime = clockBot ? clockBot.innerText : "N/A"; let oppTime = clockTop ? clockTop.innerText : "N/A"; let simpleRes = "Draw"; const mainMsg = resultEl.querySelector(".game-result-main-message, .game-over-header-title"); const subMsgEl = resultEl.querySelector(".game-result-sub-message, .game-over-header-subtitle"); const fullText = ((mainMsg ? mainMsg.innerText : resultEl.innerText.split("\n")[0]) + " " + (subMsgEl ? subMsgEl.innerText : "")).toLowerCase(); if (resultEl.classList.contains("game-result-win")) simpleRes = "Win"; else if (resultEl.classList.contains("game-result-loss")) simpleRes = "Loss"; else if (resultEl.classList.contains("game-result-draw")) simpleRes = "Draw"; else if (fullText.includes("you won")) simpleRes = "Win"; else if (fullText.includes("you lost")) simpleRes = "Loss"; else if (playingAsCode === 1 && fullText.includes("white won")) simpleRes = "Win"; else if (playingAsCode === 1 && fullText.includes("black won")) simpleRes = "Loss"; else if (playingAsCode === 2 && fullText.includes("black won")) simpleRes = "Win"; else if (playingAsCode === 2 && fullText.includes("white won")) simpleRes = "Loss"; state.history.push({ date: new Date().toLocaleString(), color: playerColor, result: simpleRes, fen, myTime, oppTime, id: Date.now() }); if (state.history.length > 200) state.history.shift(); GM_setValue("bot_history", state.history); state.hasSavedCurrentGameResult = !0; if (state.ui.histModal && state.ui.histModal.style.display !== "none") renderHistory(); } else { state.hasSavedCurrentGameResult = !1; } } function enforceBounds() { if (state.ui.panel) { const rect = state.ui.panel.getBoundingClientRect(); if (rect.right > window.innerWidth) state.ui.panel.style.width = window.innerWidth - rect.left + "px"; if (rect.bottom > window.innerHeight) state.ui.panel.style.height = window.innerHeight - rect.top + "px"; if (rect.left < 0) state.ui.panel.style.left = "0px"; if (rect.top < 0) state.ui.panel.style.top = "0px"; } requestAnimationFrame(enforceBounds); } requestAnimationFrame(enforceBounds); function updateUI() { if (!state.ui.panel) return; document.body.classList.remove("mode-cloud", "mode-local", "mode-sfonline"); document.body.classList.add(`mode-${settings.engineMode}`); if (state.ui.debugArea) state.ui.debugArea.style.display = settings.debugLogs ? "flex" : "none"; let maxD = 18; if (settings.engineMode === "local") maxD = getEngineById(settings.localModelId || "sf18_05").maxDepth; else if (settings.engineMode === "sfonline") maxD = 15; if (state.ui.lblMaxDepth) state.ui.lblMaxDepth.innerText = maxD; if (state.ui.inpDepth) state.ui.inpDepth.max = maxD; if (state.ui.inpPVDepth) state.ui.inpPVDepth.max = 45; if (state.ui.pvSettings) state.ui.pvSettings.style.display = settings.showPVArrows ? "flex" : "none"; if (state.ui.pvGradSettings) state.ui.pvGradSettings.style.display = settings.pvCustomGradient ? "flex" : "none"; if (state.ui.btnAnalyze) state.ui.btnAnalyze.disabled = state.isThinking; if (state.ui.moveResult) state.ui.moveResult.innerHTML = state.lastMoveResult; if (state.ui.liveOutput) state.ui.liveOutput.innerHTML = state.lastLiveResult; if (state.ui.delayDisplay) state.ui.delayDisplay.innerText = `Delay: ${state.calculatedDelay}s`; if (state.ui.logSent) state.ui.logSent.innerText = state.lastPayload; if (state.ui.logRec) state.ui.logRec.innerText = state.lastResponse; if (document.activeElement !== state.ui.inpDepth) state.ui.inpDepth.value = settings.depth; } function mainLoop() { state.board = document.querySelector(CONFIG.BOARD_SEL); EvalBar.create(); EvalBar.updatePosition(); if (state.board?.game && settings.autoRun) { const raw = getRawBoardFEN(); if (raw) { const clean = sanitizeFEN(raw); if (settings.hideAfterMove && state.lastSeenFEN && clean !== state.lastSeenFEN) { Visuals.removeByType('history'); Visuals.removeByType('analysis'); PV.clear(); } state.lastSeenFEN = clean; const isTurn = state.board.game.getTurn() === state.board.game.getPlayingAs(); if (isTurn && clean !== state.lastSanitizedBoardFEN) analyze(settings.depth); } } if (!state.ui.panel) createUI(); if (state.board?.game?.getPlayingAs) { try { const pa = state.board.game.getPlayingAs(); if (pa === 1 || pa === 2) state.playingAs = pa; } catch (e) {} } if (state.board?.game && settings.autoRun) { const raw = getRawBoardFEN(); if (raw) { const clean = sanitizeFEN(raw); const isTurn = state.board.game.getTurn() === state.board.game.getPlayingAs(); if (isTurn && clean !== state.lastSanitizedBoardFEN) analyze(settings.depth); } } checkForGameOver(); updateUI(); } // --- GLOBAL KEYBIND LISTENER --- document.addEventListener("keydown", (e) => { // Stop if Auto-Move is ON, or if no keybind is set if (settings.autoMove || !settings.moveKeybind || settings.moveKeybind === "None") return; // Prevent triggering if the user is typing in chat or a text box if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return; let keyName = e.key === " " ? "Space" : e.key; // If the pressed key matches the saved keybind if (keyName.toLowerCase() === settings.moveKeybind.toLowerCase()) { if (!state.board?.game || !state.currentBestMove) return; // Validate that it's actually our turn to avoid breaking the client const turn = state.board.game.getTurn(); const playingAs = state.board.game.getPlayingAs(); if (turn === playingAs) { e.preventDefault(); // Stop page scrolling if using Space/Arrows playMove(state.currentBestMove); } } }); // --- END GLOBAL KEYBIND LISTENER --- setInterval(mainLoop, CONFIG.LOOP_MS); })();