Greasy Fork is available in English.
An extremely advanced Chess.com cheat menu with three powerful Stockfish models, two online, one offline, and countless customization options.
当前为
// ==UserScript== // @name BEST Cheat for Chess.com (Stockfish 18.0.5, 18.0.0, 17.1.0, No Anti-Ban) // @namespace http://tampermonkey.net/ // @version 9.0.3 // @description An extremely advanced Chess.com cheat menu with three powerful Stockfish models, two online, one offline, 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/[email protected]/bin/stockfish-18-single.js // @run-at document-idle // ==/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+wkhG5t9t59S+iSA/XUFfA9gPyFkY7Pv9lNKTwLYX1fA9wDuI4RsbPbd/v8U4H/fA0II8Ty/mxDiA7C/Lh7wPID9hJCNzb7dTyl9EcD+ungA8Ty/mxDiA7C/pQCldC+l9EUA+1sK8Ty/hxDya0rpCwD2txTg/7kAIeR5APtbut8ghBBC2pZ/ALy683b5qZ2oAAAAAElFTkSuQmCC"; const DEFAULT_WASM_URL = "https://unpkg.com/[email protected]/bin/stockfish-18-single.wasm"; const WASM_PRESETS = [ { label: "SF 18.0.5 Single-thread (unpkg)", url: "https://unpkg.com/[email protected]/bin/stockfish-18-single.wasm" }, { label: "SF 18.0.5 Single-thread (jsDelivr)", url: "https://cdn.jsdelivr.net/npm/[email protected]/bin/stockfish-18-single.wasm" }, { label: "Custom URL...", url: "custom" }, ]; // --- 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", // "not_installed" | "loading" | "ready" | "error" 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, 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", localWasmUrl: DEFAULT_WASM_URL, localHashMB: 64, localMoveOverhead: 100, localSkillLevel: 20, localLimitStrength: false, localElo: 3190, localShowWDL: false, localMinThinkingTime: 20, localSlowMover: 100, }; 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 --- function saveSetting(key, val) { settings[key] = val; GM_setValue(`bot_${key}`, val); } function loadSettings() { Object.keys(DEFAULT_SETTINGS).forEach((k) => { const saved = GM_getValue(`bot_${k}`); if (saved !== undefined) settings[k] = saved; }); 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); 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()); }, 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 --- const EvalBar = { el: null, whiteFill: null, scoreWhiteEl: null, scoreBlackEl: null, _evalToTranslate: (ev) => 100 - (100 / (1 + Math.exp(-ev * 0.4))), create: () => { if (document.getElementById("bot-eval-bar")) { EvalBar.el = document.getElementById("bot-eval-bar"); EvalBar.whiteFill = document.getElementById("bot-eval-white-fill"); EvalBar.scoreWhiteEl = document.getElementById("bot-eval-score-white"); EvalBar.scoreBlackEl = document.getElementById("bot-eval-score-black"); return; } const bar = document.createElement("div"); bar.id = "bot-eval-bar"; bar.style.cssText = "position:fixed;width:16px;z-index:100;pointer-events:none;display:none;border-radius:3px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.6);"; bar.innerHTML = ` <div style="position:relative;width:100%;height:100%;background:#1a1a1a;border:1px solid #3a3a3a;box-sizing:border-box;border-radius:3px;overflow:hidden;"> <div id="bot-eval-white-fill" style="position:absolute;bottom:0;left:0;width:100%;height:100%;background:#ffffff;transform:translate3d(0,50%,0);transition:transform 0.35s cubic-bezier(0.4,0,0.2,1);will-change:transform;"></div> <span id="bot-eval-score-black" style="position:absolute;top:4px;left:50%;transform:translateX(-50%);font-size:8.5px;font-weight:bold;font-family:monospace;z-index:3;color:#cccccc;white-space:nowrap;writing-mode:vertical-rl;text-orientation:mixed;line-height:1;text-shadow:0 1px 2px rgba(0,0,0,0.8);"></span> <span id="bot-eval-score-white" style="position:absolute;bottom:4px;left:50%;transform:translateX(-50%);font-size:8.5px;font-weight:bold;font-family:monospace;z-index:3;color:#333333;white-space:nowrap;writing-mode:vertical-rl;text-orientation:mixed;line-height:1;text-shadow:0 1px 2px rgba(255,255,255,0.5);"></span> </div>`; document.body.appendChild(bar); EvalBar.el = bar; EvalBar.whiteFill = document.getElementById("bot-eval-white-fill"); EvalBar.scoreWhiteEl = document.getElementById("bot-eval-score-white"); EvalBar.scoreBlackEl = document.getElementById("bot-eval-score-black"); }, updatePosition: () => { if (!EvalBar.el) EvalBar.create(); const board = document.querySelector(CONFIG.BOARD_SEL); if (!board) { if (EvalBar.el) EvalBar.el.style.display = "none"; return; } const rect = board.getBoundingClientRect(); if (!rect.width) { EvalBar.el.style.display = "none"; return; } EvalBar.el.style.display = "block"; EvalBar.el.style.top = rect.top + "px"; EvalBar.el.style.left = (rect.left - 16 - 4) + "px"; EvalBar.el.style.height = rect.height + "px"; }, update: (evalScore, mate) => { if (!EvalBar.el) return; const playingAsBlack = (state.playingAs === 2); const myFillColor = playingAsBlack ? '#202020' : '#ffffff'; const oppBgColor = playingAsBlack ? '#f0f0f0' : '#1a1a1a'; const myLabelColor = playingAsBlack ? '#cccccc' : '#333333'; const oppLabelColor = playingAsBlack ? '#333333' : '#cccccc'; const innerBar = EvalBar.el.querySelector('div'); if (innerBar) innerBar.style.background = oppBgColor; if (EvalBar.whiteFill) EvalBar.whiteFill.style.background = myFillColor; let translateY = 50, scoreText = "0.0", iWin = false; if (mate !== null && mate !== undefined && mate !== 0) { const m = parseInt(mate); const whiteWinsMate = m > 0; iWin = playingAsBlack ? !whiteWinsMate : whiteWinsMate; translateY = iWin ? 0 : 100; scoreText = "M" + Math.abs(m); } else if (evalScore !== null && evalScore !== undefined) { const ev = parseFloat(evalScore); if (!isNaN(ev)) { const raw = EvalBar._evalToTranslate(ev); translateY = playingAsBlack ? (100 - raw) : raw; const whiteWinsEval = ev >= 0; iWin = playingAsBlack ? !whiteWinsEval : whiteWinsEval; const absEv = Math.abs(ev); const sign = ev > 0 ? "+" : ev < 0 ? "-" : ""; scoreText = absEv < 10 ? sign + absEv.toFixed(1) : sign + Math.round(absEv).toString(); } } if (EvalBar.whiteFill) EvalBar.whiteFill.style.transform = `translate3d(0, ${translateY}%, 0)`; const absScore = Math.abs(parseFloat(scoreText.replace(/[^0-9.]/g, ""))); const showScore = absScore > 0.1 || (mate !== null && mate !== undefined); if (EvalBar.scoreWhiteEl) { EvalBar.scoreWhiteEl.textContent = (showScore && iWin) ? scoreText : ""; EvalBar.scoreWhiteEl.style.color = myLabelColor; } if (EvalBar.scoreBlackEl) { EvalBar.scoreBlackEl.textContent = (showScore && !iWin) ? scoreText : ""; EvalBar.scoreBlackEl.style.color = oppLabelColor; } }, reset: () => { if (!EvalBar.el) return; if (EvalBar.whiteFill) EvalBar.whiteFill.style.transform = "translate3d(0,50%,0)"; if (EvalBar.scoreWhiteEl) EvalBar.scoreWhiteEl.textContent = ""; if (EvalBar.scoreBlackEl) EvalBar.scoreBlackEl.textContent = ""; } }; 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(); } function buildEngine(jsCode, wasmBytes, db) { try { 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<b.length;i++) a[i]=b.charCodeAt(i); return a; })(); self.fetch = function(url, opts) { return Promise.resolve({ ok:true, arrayBuffer:function(){ return Promise.resolve(_wasmBytes.buffer); } }); }; `; const blob = new Blob([patchCode + jsCode], { type: "application/javascript" }); state.localEngine = new Worker(URL.createObjectURL(blob)); state.localEngine.onerror = (e) => { handleError("Local Engine Error", e); setEngineStatus("error", e.message || "Worker error"); state.localEngine = null; state.engineLoadingInProgress = false; }; state.localEngine.onmessage = handleLocalMessage; // SF18 single-file wires its own onmessage internally. // Send config commands; the internal queue handles ordering. ["ucinewgame", `setoption name Hash value ${settings.localHashMB}`, `setoption name Move Overhead value ${settings.localMoveOverhead}`, `setoption name Minimum Thinking Time value ${settings.localMinThinkingTime}`, `setoption name Slow Mover value ${settings.localSlowMover}`, `setoption name UCI_ShowWDL value ${settings.localShowWDL}`, `setoption name UCI_LimitStrength value ${settings.localLimitStrength}`, `setoption name UCI_Elo value ${settings.localElo}`, `setoption name Skill Level value ${settings.localSkillLevel}`, "setoption name MultiPV value 1" ].forEach(c => state.localEngine.postMessage(c)); state.engineLoadingInProgress = false; setEngineStatus("ready", ""); state.lastMoveResult = "✅ SF18 Ready."; console.log("Stockfish 18 loaded."); updateUI(); // If we have a db and want to persist (already saved before calling buildEngine for reinstall) } catch (e) { handleError("Engine Build Fail", e); state.engineLoadingInProgress = false; setEngineStatus("error", e.message || "Build failed"); } } function downloadAndInstall(jsCode, wasmUrl, db) { setEngineStatus("loading", "Downloading WASM..."); state.lastMoveResult = "⏳ Downloading SF18 WASM..."; updateUI(); GM_xmlhttpRequest({ method: "GET", url: wasmUrl, responseType: "arraybuffer", onload: (wasmRes) => { try { const wasmBytes = new Uint8Array(wasmRes.response); if (db) { try { const tx = db.transaction("wasm", "readwrite"); tx.objectStore("wasm").put(wasmBytes, "bytes"); } catch (e2) {} } buildEngine(jsCode, wasmBytes, db); } catch (e) { handleError("Engine Load Fail", e); state.engineLoadingInProgress = false; setEngineStatus("error", e.message || "Load failed"); } }, onerror: (e) => { handleError("WASM Download Fail", e); state.engineLoadingInProgress = false; setEngineStatus("error", "Download failed — check URL"); }, }); } function loadLocalEngine() { if (state.localEngine || state.engineLoadingInProgress) return; state.engineLoadingInProgress = true; state.isThinking = false; const jsCode = GM_getResourceText("stockfish.js"); if (!jsCode) { handleError("Engine Load Fail", "SF18 JS resource missing"); state.engineLoadingInProgress = false; setEngineStatus("error", "JS resource missing"); return; } const wasmUrl = settings.localWasmUrl || DEFAULT_WASM_URL; setEngineStatus("loading", "Opening cache..."); const dbReq = indexedDB.open("sf18cache", 1); dbReq.onupgradeneeded = (e) => e.target.result.createObjectStore("wasm"); dbReq.onsuccess = (e) => { const db = e.target.result; const getReq = db.transaction("wasm", "readonly").objectStore("wasm").get("bytes"); getReq.onsuccess = (e2) => { if (e2.target.result) { setEngineStatus("loading", "Loading from cache..."); state.lastMoveResult = "⚡ SF18 loading from cache..."; updateUI(); buildEngine(jsCode, e2.target.result, db); } else { downloadAndInstall(jsCode, wasmUrl, db); } }; getReq.onerror = () => downloadAndInstall(jsCode, wasmUrl, null); }; dbReq.onerror = () => downloadAndInstall(jsCode, wasmUrl, null); } function reinstallEngine() { if (state.localEngine) { try { state.localEngine.terminate(); } catch (e) {} state.localEngine = null; } state.engineLoadingInProgress = false; setEngineStatus("loading", "Clearing cache..."); const jsCode = GM_getResourceText("stockfish.js"); if (!jsCode) { setEngineStatus("error", "JS resource missing"); return; } const wasmUrl = settings.localWasmUrl || DEFAULT_WASM_URL; const dbReq = indexedDB.open("sf18cache", 1); dbReq.onupgradeneeded = (e) => e.target.result.createObjectStore("wasm"); dbReq.onsuccess = (e) => { const db = e.target.result; try { const tx = db.transaction("wasm", "readwrite"); const del = tx.objectStore("wasm").delete("bytes"); del.onsuccess = () => { state.engineLoadingInProgress = true; downloadAndInstall(jsCode, wasmUrl, db); }; del.onerror = () => { state.engineLoadingInProgress = true; downloadAndInstall(jsCode, wasmUrl, db); }; } catch (e2) { state.engineLoadingInProgress = true; downloadAndInstall(jsCode, wasmUrl, null); } }; dbReq.onerror = () => { state.engineLoadingInProgress = true; downloadAndInstall(jsCode, wasmUrl, null); }; } function uninstallEngine() { if (state.localEngine) { try { state.localEngine.terminate(); } catch (e) {} state.localEngine = null; } state.engineLoadingInProgress = false; const dbReq = indexedDB.open("sf18cache", 1); dbReq.onupgradeneeded = (e) => e.target.result.createObjectStore("wasm"); dbReq.onsuccess = (e) => { const db = e.target.result; try { const tx = db.transaction("wasm", "readwrite"); tx.objectStore("wasm").delete("bytes"); tx.oncomplete = () => setEngineStatus("not_installed", ""); } catch (e2) { setEngineStatus("not_installed", ""); } }; dbReq.onerror = () => 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; // engine loading async — mainLoop will retry once ready } state.localEval = null; state.localMate = null; state.localPV = null; state.localDepth = null; const actualDepth = Math.min(depth, 25); 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; 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}: <span style="font-weight:bold; color:var(--bot-primary);">${best}</span>`; } state.lastLiveResult = ` <div style="display:flex; justify-content:space-between; align-items:center; font-weight:bold;"> <div style="display:flex; align-items:center; gap: 8px;"> <span style="color:var(--bot-primary); font-size:1.1em;">${scoreTxt}</span> <span style="font-size:0.85em; color:${statusData.color}; font-weight:bold;">${statusData.text}</span> </div> <span style="font-size:0.7em; color:#aaa; font-weight:normal;">(${duration}s)</span> </div>`; 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 ? `<span style="font-size:0.7em; color:#aaa; font-weight:normal;">(${duration}s)</span>` : ""; state.lastMoveResult = `✅ Best: <span style="font-weight:bold; color:var(--bot-primary);">${bestMove}</span>`; let wcHtml = ""; if (winChance) wcHtml = `<span style="color:#aaa; font-size:0.8em;">(${Math.round(winChance)}%)</span>`; else if (depth) wcHtml = `<span style="font-size:0.8em; color:#aaa;">(D${depth})</span>`; state.lastLiveResult = ` <div style="display:flex; justify-content:space-between; align-items:center; font-weight:bold;"> <div style="display:flex; align-items:center; gap: 8px;"> <span style="color:var(--bot-primary); font-size:1.1em;">${scoreTxt}</span> <span style="font-size:0.85em; color:${statusData.color}; font-weight:bold;">${statusData.text}</span> </div> <div>${wcHtml} ${durHtml}</div> </div> <div style="margin-top:5px; font-size:0.85em; color:#bbb; width:100%; max-width:100%; box-sizing:border-box; word-wrap:break-word; overflow-wrap:anywhere; white-space:normal;"> <span style="color:#888;">PV:</span> ${pvStr} </div>`; 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; } } } 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 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; } 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"; const style = ` ${S} { --bot-bg:${settings.themeBg}; --bot-b:${settings.themeBorder}; --bot-p:${settings.themePrimary}; --bot-t:${settings.themeText}; } ${SM} { --bot-bg:${settings.themeBg}; --bot-b:${settings.themeBorder}; --bot-p:${settings.themePrimary}; --bot-t:${settings.themeText}; } ${SH} { --bot-bg:${settings.themeBg}; --bot-b:${settings.themeBorder}; --bot-p:${settings.themePrimary}; --bot-t:${settings.themeText}; } ${SL} { --bot-bg:${settings.themeBg}; --bot-b:${settings.themeBorder}; --bot-p:${settings.themePrimary}; --bot-t:${settings.themeText}; } ${S} * { box-sizing: border-box; } ${SM} * { box-sizing: border-box; } ${SH} * { box-sizing: border-box; } ${SL} * { box-sizing: border-box; } ${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:sans-serif; box-shadow:-4px 0 15px rgba(0,0,0,0.5); font-size:14px; display:flex; flex-direction:column; resize:both; overflow:hidden; opacity: ${settings.menuOpacity}; } ${S}.minified { width: 34px !important; height: 34px !important; resize: none; min-height: 0 !important; min-width: 0 !important; overflow: hidden !important; border: 1px solid var(--bot-b); background: var(--bot-p); 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: 4px; } ${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: 28px !important; height: 28px !important; display: block; } #panelHeader { background:var(--bot-p); color:#000; padding:10px; font-weight:bold; display:flex; justify-content:space-between; align-items:center; cursor:move; flex:none; user-select:none; height:38px; } #panelContent { padding:15px; display:flex; flex-direction:column; gap:10px; overflow-y:auto; flex:1; min-height: 0; } ${S} .sect { border-top:1px solid #333; padding-top:10px; display:flex; flex-direction:column; gap:8px; } ${SM} .sect, ${SL} .sect { border-top:1px solid #333; padding-top:10px; display:flex; flex-direction:column; gap:8px; } ${S} .sect-title { font-size:0.85em; color:#aaa; font-weight:bold; text-transform:uppercase; margin-bottom:4px; } ${SM} .sect-title, ${SL} .sect-title { font-size:0.85em; color:#aaa; font-weight:bold; text-transform:uppercase; margin-bottom:4px; } ${S} .row { display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom:6px; } ${SM} .row, ${SL} .row { display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom:6px; } ${S} input, ${S} select, ${SM} input, ${SM} select, ${SH} input, ${SH} select, ${SL} input, ${SL} select { background:rgba(0,0,0,0.2); color:var(--bot-t); border:1px solid var(--bot-b); padding:4px; border-radius:4px; } ${S} input[type="number"], ${SM} input[type="number"], ${SH} input[type="number"], ${SL} input[type="number"] { width: 60px; } ${S} select, ${SM} select, ${SH} select, ${SL} select { width: 120px; } ${S} input[type="text"], ${SM} input[type="text"], ${SL} input[type="text"] { flex:1; } ${S} input[type=range], ${SM} input[type=range] { -webkit-appearance: none; width: 100%; background: transparent; padding: 0; margin: 0; border: none; } ${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: 6px; cursor: pointer; background: var(--bot-b); border-radius: 3px; } ${S} input[type=range]::-webkit-slider-thumb, ${SM} input[type=range]::-webkit-slider-thumb { height: 16px; width: 16px; border-radius: 50%; background: var(--bot-t); cursor: pointer; -webkit-appearance: none; margin-top: -5px; border: 1px solid rgba(0,0,0,0.3); box-shadow: 0 1px 3px rgba(0,0,0,0.3); } #sliderH { background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00) !important; } #sliderH::-webkit-slider-thumb { background: #fff !important; border: 1px solid #000 !important; } ${S} button, ${SM} button, ${SH} button, ${SL} button { background:var(--bot-p); border:none; padding:10px; color:#000; font-weight:bold; cursor:pointer; border-radius:4px; } ${S} button:disabled, ${SM} button:disabled, ${SL} button:disabled { opacity:0.6; cursor:not-allowed; } #custBtn { background: #4fc3f7 !important; color: #000 !important; margin-top: 5px; } #histBtn { background: #b39ddb !important; color: #000 !important; margin-top: 5px; } #localBtn { background: #ffcc80 !important; color: #000 !important; margin-top: 5px; } .log-box { background:rgba(0,0,0,0.5); padding:8px; font-family:monospace; font-size:0.75em; border-radius:4px; overflow-y:auto; word-break:break-all; white-space:pre-wrap; border:1px solid var(--bot-b); height:100px; resize:vertical; user-select: text !important; -webkit-user-select: text !important; cursor: text; } #statusBox { background:rgba(0,0,0,0.2); padding:8px; border:1px solid #00bcd4; border-radius:4px; font-size:0.9em; min-height:40px; width:100%; flex-shrink:0; display:flex; flex-direction:column; gap:5px; } ${SO} { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); z-index:10000; display:none; justify-content:center; align-items:center; } ${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}; } ${SH} { width: 600px; height: 600px; } ${SM} * { color:var(--bot-t); } ${SL} * { color:var(--bot-t); } ${SM} label, ${SH} label, ${SL} label { opacity:1 !important; font-weight:600; font-size:0.9em; } ${SM} input[type="color"], ${SH} input[type="color"] { height:24px; padding:0; width:40px; cursor:pointer; border:none; } ${SM} select, ${SH} select, ${SL} select { height:24px; padding:0 4px; font-size:0.9em; } ${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; } ${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:45px; text-align:center; } #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.85em; } #histTable th { background:var(--bot-b); color:var(--bot-p); position:sticky; top:0; z-index:1; } #histTable th, #histTable td { border-bottom:1px solid var(--bot-b); padding:6px; text-align:left; color:var(--bot-t); } #histTable tr:hover { background:var(--bot-b); filter:brightness(1.2); } .hist-win { color:#81b64c; font-weight:bold; } .hist-loss { color:#ff5555; font-weight:bold; } .hist-draw { color:#aaaaaa; font-weight:bold; } .hist-fen { max-width:100px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; cursor:pointer; color:#888; text-decoration:underline dotted; } .btn-del { background:#ff5555 !important; color:white !important; padding:2px 6px; border-radius:3px; font-size:0.7em; cursor:pointer; border:none; } .hist-controls { display:flex; justify-content:space-between; align-items:center; margin-top:10px; } #histEmpty { padding:20px; text-align:center; color:#888; } #fenTooltip { position:fixed; border:3px solid #333; background:#222; z-index:10001; display:none; pointer-events:none; box-shadow:0 4px 15px rgba(0,0,0,0.5); } .fen-board { display:grid; grid-template-columns:repeat(8, 1fr); width:240px; height:240px; border:2px solid #555; } .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 { display:flex; justify-content:space-between; align-items:center; padding:15px; border-bottom:1px solid var(--bot-b); } .modal-tabs { display:flex; border-bottom:1px solid var(--bot-b); } ${SM} .tab-btn { flex:1; background:transparent !important; border:none !important; padding:10px; color:var(--bot-t); cursor:pointer; opacity:0.7; border-bottom:2px solid transparent !important; } ${SM} .tab-btn.active { opacity:1; border-bottom:2px solid var(--bot-p) !important; font-weight:bold; } .modal-content { padding:15px; overflow-y:auto; flex:1; } ${S} .slider-group, ${SM} .slider-group { display:flex; align-items:center; gap:8px; 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:45px; text-align:center; } .adv-toggle { cursor:pointer; font-size:0.8em; color:var(--bot-p); text-decoration:underline; margin-top:5px; display:inline-block; } ${SM} .modal-content .row { display:flex; align-items:center; margin-bottom:12px; } ${SM} .modal-content .row label { flex:0 0 120px; 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; } .adv-sect { margin-top:10px; padding-left:10px; border-left:2px solid var(--bot-b); display:flex; flex-direction:column; gap:8px; } .theme-presets { display:flex; gap:10px; margin-bottom:10px; } ${SM} .theme-btn { flex:1; padding:5px; border:1px solid var(--bot-b) !important; cursor:pointer; background:rgba(0,0,0,0.2) !important; color:var(--bot-t) !important; } /* Local Settings modal specific */ #localEngineStatus { font-weight:bold; font-size:1em; } #localEngineStatusMsg { font-size:0.8em; color:#aaa; margin-top:2px; min-height:14px; } .local-action-btn { padding:8px 14px !important; font-size:0.85em !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:8px 10px; font-size:0.82em; font-family:monospace; color:#bbb; word-break:break-all; } ${SL} input[type="text"] { width:100%; font-size:0.85em; } ${SL} select { width:100%; } `; const fullHTML = `<style>${style}</style>` + ` <div id="enginePanel" class="${isMini ? "minified" : ""}"> <div id="panelHeader"> <div style="display:flex; align-items:center; gap:5px;"> <span>Menu</span> <span id="minBtn" style="cursor:pointer; display:flex; align-items:center; justify-content:center; width:100%; height:100%;">${isMini ? `<img src="${STOCKFISH_ICON}">` : "▼"}</span> </div> <button id="btnReset" style="padding:2px 8px; font-size:0.8em; background:#0002; color:#000; cursor:pointer;">Reset Defaults</button> </div> <div id="panelContent"> <div id="statusBox">${state.lastLiveResult}</div> <div id="moveResult" style="background:rgba(0,0,0,0.2); padding:5px; border-radius:4px; text-align:center;">${state.lastMoveResult}</div> <div class="sect"> <div class="sect-title">Engine Config</div> <div class="row"> <label>Model</label> <select id="selMode" style="width:240px;"> <option value="cloud">SF 18.0.0 (cloud 0.25-0.48s)</option> <option value="sfonline">SF 17.1.0 (cloud 0.15-11.0s)</option> <option value="local">SF 18.0.5 (local 0.0-28.0s)</option> </select> </div> <div class="row"><label>Depth (Max <span id="lblMaxDepth">18</span>)</label><input type="number" id="inpDepth" min="1" max="18" value="${settings.depth}"></div> <div class="row show-cloud"><label>Max Time (ms)</label><input type="number" id="inpTime" value="${settings.maxThinkingTime}"></div> <div class="row show-cloud"><label>Search</label><input type="text" id="inpSearch" value="${settings.searchMoves}"></div> </div> <div class="sect"> <div class="sect-title" style="display:flex; justify-content:space-between; align-items:center;"> PV Display <input type="checkbox" id="chkPV" ${settings.showPVArrows ? "checked" : ""}> </div> <div id="pvSettings" style="display:none;"> <div class="row"><label>Depth (1-45)</label><div class="slider-group"><input type="range" id="inpPVDepth" min="1" max="45" step="1" value="${settings.pvDepth}"><input type="number" id="inpPVDepthNum" min="1" max="45" value="${settings.pvDepth}"></div></div> <div class="row" style="padding-top:3px;"><label>Show Numbers</label><input type="checkbox" id="chkPVNums" ${settings.pvShowNumbers ? "checked" : ""}></div> <div class="row" style="padding-top:5px;"><label>Custom Gradient</label><input type="checkbox" id="chkPVGrad" ${settings.pvCustomGradient ? "checked" : ""}></div> <div id="pvGradSettings" style="display:none; padding-left:10px; border-left:2px solid #333; margin-top:5px;"> <div class="row"><label>Start Color</label><input type="color" id="inpPVStart" value="${settings.pvStartColor}"></div> <div class="row"><label>End Color</label><input type="color" id="inpPVEnd" value="${settings.pvEndColor}"></div> </div> </div> </div> <div class="sect"> <div class="sect-title">Automation</div> <div class="row"> <label><input type="checkbox" id="chkRun" ${settings.autoRun ? "checked" : ""}> Auto-Analyze</label> <label><input type="checkbox" id="chkMove" ${settings.autoMove ? "checked" : ""}> Auto-Move</label> <label><input type="checkbox" id="chkQueue" ${settings.autoQueue ? "checked" : ""}> Auto-Queue</label> </div> <div class="row"><label>Randomized Delay (s)</label><div style="display:flex; gap:5px;"><input type="number" id="inpMin" style="width:50px" value="${settings.minDelay}"><span>-</span><input type="number" id="inpMax" style="width:50px" value="${settings.maxDelay}"></div></div> <div style="font-size:0.7em; color:#888; text-align:right;" id="delayDisplay">Next: N/A</div> </div> <button id="btnAnalyze">Analyze</button> <button id="custBtn">Visuals & Theme</button> <button id="histBtn">Game History</button> <button id="localBtn">Local Settings</button> <div class="sect"> <div class="row"><label style="cursor:pointer"><input type="checkbox" id="chkDebug" ${settings.debugLogs ? "checked" : ""}> Show Debug Logs</label></div> <div id="debugArea" style="display:${settings.debugLogs ? "block" : "none"}"> <div class="log-box" id="sentCommandOutput"></div> <div class="log-box" id="receivedMessageOutput"></div> </div> </div> </div> </div> <div id="modalOv"> <div id="modal"> <div class="modal-header"> <h3 style="margin:0; color:var(--bot-p);">Settings</h3> <button id="modalClose" style="padding:2px 8px; font-weight:bold; cursor:pointer;">×</button> </div> <div class="modal-tabs"> <button class="tab-btn active" id="tabMove">Move Display</button> <button class="tab-btn" id="tabTheme">Menu Theme</button> </div> <div class="modal-content" id="tabContentMove"> <div class="sect" style="border:none; padding:0;"> <div class="row" style="margin-bottom: 20px;"><label>Visual Type</label><select id="visType" style="width:120px; height:24px;"><option value="boxes">Boxes</option><option value="arrow">Arrow</option><option value="outline">Outline</option></select></div> <div class="row" style="margin-bottom: 12px;"> <label>Display Duration</label> <div class="slider-group"> <input type="range" id="visDuration" min="0" max="100" step="1" value="100"> <span id="visDurationText" style="width:70px; text-align:right; font-size:0.9em; font-family:monospace;">Forever</span> </div> </div> <div class="row" id="rowFadeOut" style="display:none; margin-bottom: 12px;"> <label>Fade Out</label> <div style="flex:1; display:flex; align-items:center;"> <input type="checkbox" id="chkFadeOut" style="width: 18px; height: 18px;"> </div> </div> <div class="row" style="margin-bottom: 12px;"> <label>Hide After Move</label> <div style="flex:1; display:flex; align-items:center;"> <input type="checkbox" id="chkHideAfterMove" style="width: 18px; height: 18px;" ${settings.hideAfterMove ? "checked" : ""}> </div> </div> </div> <div class="sect"> <div class="sect-title">Basic Settings</div> <div style="display:flex; flex-direction:column; gap:10px;"> <div class="row" style="width:100%; margin-bottom: 10px;"> <div id="colorPreview" style="width:30px; height:30px; border-radius:50%; border:2px solid #555; background:${settings.highlightColor}; flex:0 0 30px;"></div> <div class="rgb-inputs"> <input type="number" id="inpR" min="0" max="255" placeholder="R"> <input type="number" id="inpG" min="0" max="255" placeholder="G"> <input type="number" id="inpB" min="0" max="255" placeholder="B"> </div> </div> <div class="row"><label>Hue</label><div class="slider-group"><input type="range" id="sliderH" min="0" max="360" value="${state.h}"><input type="number" id="sliderHNum" min="0" max="360" value="${Math.round(state.h)}"></div></div> <div class="row"><label>Saturation</label><div class="slider-group"><input type="range" id="sliderS" min="0" max="100" value="${state.s}"><input type="number" id="sliderSNum" min="0" max="100" value="${Math.round(state.s)}"><span>%</span></div></div> <div class="row"><label>Brightness</label><div class="slider-group"><input type="range" id="sliderL" min="0" max="100" value="${state.l}"><input type="number" id="sliderLNum" min="0" max="100" value="${Math.round(state.l)}"><span>%</span></div></div> <div class="row" style="width:100%; margin-top:5px; margin-bottom: 20px;"><label>Hex</label><input type="text" id="inpHex" style="text-transform:uppercase; text-align:center;"></div> </div> </div> <div class="sect"> <div class="adv-toggle" id="advToggle">▼ Advanced Visual Settings</div> <div class="adv-sect" id="advSect" style="display:none;"> <div id="visBoxSettings"> <div class="row"><label>Inner Opacity</label><div class="slider-group"><input type="range" id="visInnerOp" min="0" max="1" step="0.01" value="${settings.innerOpacity}"><input type="number" id="visInnerOpNum" min="0" max="100" value="${Math.round(settings.innerOpacity*100)}"><span>%</span></div></div> <div class="row"><label>Outer Opacity</label><div class="slider-group"><input type="range" id="visOuterOp" min="0" max="1" step="0.01" value="${settings.outerOpacity}"><input type="number" id="visOuterOpNum" min="0" max="100" value="${Math.round(settings.outerOpacity*100)}"><span>%</span></div></div> <div class="row"><label>Gradient Bias</label><div class="slider-group"><input type="range" id="visBias" min="0" max="100" step="1" value="${settings.gradientBias}"><input type="number" id="visBiasNum" min="0" max="100" value="${settings.gradientBias}"><span>%</span></div></div> </div> <div id="visArrowSettings" style="display:none;"> <div class="row"><label>Arrow Opacity</label><div class="slider-group"><input type="range" id="visArrowOp" min="0" max="1" step="0.01" value="${settings.arrowOpacity}"><input type="number" id="visArrowOpNum" min="0" max="100" value="${Math.round(settings.arrowOpacity*100)}"><span>%</span></div></div> <div class="row"><label>Arrow Width</label><div class="slider-group"><input type="range" id="visArrowWidth" min="5" max="50" step="1" value="${settings.arrowWidth}"><input type="number" id="visArrowWidthNum" min="5" max="50" value="${settings.arrowWidth}"><span>px</span></div></div> </div> <div id="visOutlineSettings" style="display:none;"> <div class="row"><label>Line Opacity</label><div class="slider-group"><input type="range" id="visOutOp" min="0" max="1" step="0.01" value="${settings.visualOutlineOpacity}"><input type="number" id="visOutOpNum" min="0" max="100" value="${Math.round(settings.visualOutlineOpacity*100)}"><span>%</span></div></div> <div class="row"><label>Line Width</label><div class="slider-group"><input type="range" id="visOutWidth" min="1" max="10" step="1" value="${settings.visualOutlineWidth}"><input type="number" id="visOutWidthNum" min="1" max="10" value="${settings.visualOutlineWidth}"><span>px</span></div></div> <div class="row"><label>Glow Effect</label><input type="checkbox" id="visOutGlow" ${settings.visualOutlineGlow ? "checked" : ""}></div> <div class="row"><label>Glow Radius</label><div class="slider-group"><input type="range" id="visOutGlowRad" min="1" max="50" step="1" value="${settings.visualOutlineGlowRadius}"><input type="number" id="visOutGlowRadNum" min="1" max="50" value="${settings.visualOutlineGlowRadius}"><span>px</span></div></div> </div> </div> </div> </div> <div class="modal-content" id="tabContentTheme" style="display:none;"> <div class="theme-presets"> <button class="theme-btn" id="btnThemeDark">Dark Mode</button> <button class="theme-btn" id="btnThemeLight">Light Mode</button> </div> <div class="sect"> <div class="sect-title">Menu Position</div> <div class="row"> <label>Panel Position</label> <select id="selMenuPos"> <option value="custom">Custom (Drag)</option> <option value="top-left">Top Left</option> <option value="top-right">Top Right</option> <option value="bottom-left">Bottom Left</option> <option value="bottom-right">Bottom Right</option> </select> </div> </div> <div class="sect"> <div class="row" style="margin-bottom: 20px;"><label>Menu Opacity</label><div class="slider-group"><input type="range" id="inpMenuOp" min="0.1" max="1" step="0.01" value="${settings.menuOpacity}"><input type="number" id="inpMenuOpNum" min="10" max="100" value="${Math.round(settings.menuOpacity*100)}"><span>%</span></div></div> </div> <div class="sect"> <div class="sect-title">Custom Colors</div> <div class="row"><label>Background</label><input type="color" id="colBg" value="${settings.themeBg}"></div> <div class="row"><label>Text Color</label><input type="color" id="colTxt" value="${settings.themeText}"></div> <div class="row"><label>Border Color</label><input type="color" id="colBorder" value="${settings.themeBorder}"></div> <div class="row"><label>Primary/Accent</label><input type="color" id="colPrim" value="${settings.themePrimary}"></div> </div> </div> </div> </div> <div id="histModalOv"> <div id="histModal"> <div class="modal-header"> <h3 style="margin:0; color:#8e44ad;">Game History</h3> <button id="histModalClose" style="padding:2px 8px; font-weight:bold; cursor:pointer;">×</button> </div> <div id="histTableContainer" style="padding:0 15px;"> <table id="histTable"> <thead> <tr><th>Date</th><th>Color</th><th>Result</th><th>Clock</th><th>FEN</th><th></th></tr> </thead> <tbody id="histBody"></tbody> </table> </div> <div class="hist-controls" style="padding:15px;"> <label><input type="checkbox" id="chkHistory" ${settings.enableHistory ? "checked" : ""}> Recording Enabled</label> <button id="btnClearHist" style="background:#ff5555 !important; padding:5px 10px; color:white !important; font-size:0.8em;">Delete All</button> </div> </div> </div> <div id="localModalOv"> <div id="localModal"> <div class="modal-header"> <h3 style="margin:0; color:#e67e22;">Local Engine Settings</h3> <button id="localModalClose" style="padding:2px 8px; font-weight:bold; cursor:pointer;">×</button> </div> <div class="modal-content" style="display:flex; flex-direction:column; gap:0;"> <div class="sect" style="border:none; padding:10px 0 0 0;"> <div class="sect-title">Engine Status</div> <div id="localEngineStatus" style="font-weight:bold; font-size:1em;">❌ Not Installed</div> <div id="localEngineStatusMsg" style="font-size:0.8em; color:#aaa; margin-top:2px; min-height:14px;"></div> <div style="display:flex; gap:8px; margin-top:10px; flex-wrap:wrap;"> <button id="btnLocalInstall" class="local-action-btn local-btn-install">Install / Load</button> <button id="btnLocalReinstall" class="local-action-btn local-btn-reinstall">Reinstall (Force Re-download)</button> <button id="btnLocalUninstall" class="local-action-btn local-btn-uninstall">Uninstall</button> </div> </div> <div class="sect"> <div class="sect-title">WASM Source</div> <div class="row" style="flex-direction:column; align-items:flex-start; gap:6px;"> <label style="font-size:0.85em;">Preset</label> <select id="localWasmPreset" style="width:100%;"> ${WASM_PRESETS.map(p => `<option value="${p.url}"${settings.localWasmUrl === p.url ? " selected" : ""}>${p.label}</option>`).join("")} </select> </div> <div class="row" style="flex-direction:column; align-items:flex-start; gap:6px; margin-top:6px;" id="localCustomUrlRow"> <label style="font-size:0.85em;">Custom WASM URL</label> <input type="text" id="localWasmUrl" value="${settings.localWasmUrl}" placeholder="https://...wasm" style="width:100%;font-size:0.82em;"> </div> <div style="font-size:0.75em; color:#888; margin-top:4px;">WASM is cached in IndexedDB after first download. Use Reinstall to apply a new URL.</div> </div> <div class="sect"> <div class="sect-title">Engine Options</div> <div class="row"><label>Hash Size (MB)</label><input type="number" id="localHashMB" min="1" max="2048" value="${settings.localHashMB}" style="width:70px;"></div> <div class="row"><label>Move Overhead (ms)</label><input type="number" id="localMoveOverhead" min="0" max="5000" value="${settings.localMoveOverhead}" style="width:70px;"></div> <div class="row" style="align-items:flex-start; flex-direction:column; gap:6px;"> <div style="display:flex; justify-content:space-between; align-items:center; width:100%;"> <label>Skill Level (0–20)</label> <input type="number" id="localSkillLevel" min="0" max="20" value="${settings.localSkillLevel}" style="width:55px;"> </div> <input type="range" id="localSkillLevelRange" min="0" max="20" step="1" value="${settings.localSkillLevel}" style="width:100%; margin:0;"> <div style="font-size:0.75em; color:#888;">20 = full strength. Below 20 intentionally weakens play.</div> </div> <div class="row" style="margin-top:8px;"> <label>Limit to Elo</label> <input type="checkbox" id="localLimitStrength" ${settings.localLimitStrength ? "checked" : ""} style="width:18px;height:18px;"> </div> <div class="row" id="localEloRow" style="${settings.localLimitStrength ? "" : "display:none;"}"> <label>Target Elo (1320–3190)</label> <input type="number" id="localElo" min="1320" max="3190" value="${settings.localElo}" style="width:70px;"> </div> <div style="font-size:0.75em; color:#888; margin-top:2px;">Elo limit overrides Skill Level when enabled.</div> </div> <div class="sect"> <div class="adv-toggle" id="localAdvToggle" style="cursor:pointer; font-size:0.85em; color:var(--bot-p); text-decoration:underline; display:inline-block; margin-bottom:6px;">▼ Advanced Options</div> <div id="localAdvSect" style="display:none; padding-left:10px; border-left:2px solid var(--bot-b); display:none; flex-direction:column; gap:10px;"> <div class="row" style="margin-top:8px;"> <label>Show WDL in output</label> <input type="checkbox" id="localShowWDL" ${settings.localShowWDL ? "checked" : ""} style="width:18px;height:18px;"> </div> <div style="font-size:0.75em; color:#888; margin-bottom:4px;">Adds win/draw/loss % to each info line in debug logs.</div> <div class="row"> <label>Min Thinking Time (ms)</label> <input type="number" id="localMinThinkingTime" min="0" max="5000" value="${settings.localMinThinkingTime}" style="width:70px;"> </div> <div style="font-size:0.75em; color:#888;">Minimum ms engine spends per move regardless of time control.</div> <div class="row"> <label>Slow Mover (10–1000)</label> <input type="number" id="localSlowMover" min="10" max="1000" value="${settings.localSlowMover}" style="width:70px;"> </div> <div style="font-size:0.75em; color:#888; margin-bottom:4px;">Lower = faster moves, higher = engine uses more of its time budget. Default 100.</div> </div> </div> <div class="sect"> <div class="sect-title">JS Source (from @resource)</div> <div class="info-box" id="localJsSrc">https://unpkg.com/[email protected]/bin/stockfish-18-single.js</div> <div style="font-size:0.75em; color:#888; margin-top:4px;">Fixed at install time. Change in @resource header to update.</div> </div> </div> </div> </div> <div id="fenTooltip"></div> `; 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"), 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(); // Sync initial engine status display if (state.localEngine) setEngineStatus("ready", ""); else updateLocalSettingsUI(); // Bindings 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 = () => { updateLocalSettingsUI(); state.ui.localModal.style.display = "flex"; }; state.ui.localModalClose.onclick = () => (state.ui.localModal.style.display = "none"); // Local Settings bindings document.getElementById("btnLocalInstall").onclick = () => { loadLocalEngine(); updateLocalSettingsUI(); }; document.getElementById("btnLocalReinstall").onclick = () => reinstallEngine(); document.getElementById("btnLocalUninstall").onclick = () => { if (confirm("Uninstall local engine and clear WASM cache?")) uninstallEngine(); }; const wasmPresetSel = document.getElementById("localWasmPreset"); const wasmUrlInp = document.getElementById("localWasmUrl"); const customUrlRow = document.getElementById("localCustomUrlRow"); const syncCustomUrlVisibility = () => { const isCustom = wasmPresetSel.value === "custom"; customUrlRow.style.display = isCustom ? "block" : "none"; if (!isCustom) { wasmUrlInp.value = wasmPresetSel.value; saveSetting("localWasmUrl", wasmPresetSel.value); } }; wasmPresetSel.onchange = syncCustomUrlVisibility; wasmUrlInp.oninput = (e) => saveSetting("localWasmUrl", e.target.value.trim()); syncCustomUrlVisibility(); // Helper: send option live to running engine if available const sendOpt = (name, val) => { if (state.localEngine) state.localEngine.postMessage(`setoption name ${name} value ${val}`); }; document.getElementById("localHashMB").oninput = (e) => { const v = parseInt(e.target.value) || 64; saveSetting("localHashMB", v); sendOpt("Hash", v); }; document.getElementById("localMoveOverhead").oninput = (e) => { const v = parseInt(e.target.value) || 100; saveSetting("localMoveOverhead", v); sendOpt("Move Overhead", v); }; // Skill Level — range + number synced const skillNum = document.getElementById("localSkillLevel"); const skillRange = document.getElementById("localSkillLevelRange"); const applySkill = (v) => { saveSetting("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)); }; // Limit Strength + Elo const limitChk = document.getElementById("localLimitStrength"); const eloRow = document.getElementById("localEloRow"); const eloInp = document.getElementById("localElo"); limitChk.onchange = (e) => { saveSetting("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)); saveSetting("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"; }; document.getElementById("localShowWDL").onchange = (e) => { saveSetting("localShowWDL", e.target.checked); sendOpt("UCI_ShowWDL", e.target.checked); }; document.getElementById("localMinThinkingTime").oninput = (e) => { const v = parseInt(e.target.value) || 20; saveSetting("localMinThinkingTime", v); sendOpt("Minimum Thinking Time", v); }; document.getElementById("localSlowMover").oninput = (e) => { const v = Math.min(1000, Math.max(10, parseInt(e.target.value) || 100)); saveSetting("localSlowMover", v); sendOpt("Slow Mover", v); }; const toggleMin = () => { const isMini = state.ui.panel.classList.toggle("minified"); saveSetting("isMini", isMini); state.ui.minBtn.innerHTML = isMini ? `<img src="${STOCKFISH_ICON}">` : "▼"; }; 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 = `<img src="${STOCKFISH_ICON}">`; 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 = !1; if (settings.engineMode === "local") loadLocalEngine(); 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); if (val >= 100) { durText.innerText = "Forever"; rowFade.style.display = "none"; saveSetting("visualDuration", 0); } else if (val <= 0) { durText.innerText = "Disabled"; rowFade.style.display = "none"; saveSetting("visualDuration", -1); } else { const secs = sliderToSeconds(val); durText.innerText = secs.toFixed(1) + "s"; rowFade.style.display = "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"; 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"); 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(); }; }); state.ui.inpHex.onchange = (e) => { if (/^#[0-9A-F]{6}$/i.test(e.target.value)) { const rgb=hexToRgb(e.target.value); 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 = '<div class="fen-board">'; 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 += `<div class="fen-sq ${isDark ? "dark" : "light"}" ${bg}></div>`; } return html + "</div>"; } function renderHistory() { if (!state.ui.histBody) return; state.ui.histBody.innerHTML = ""; if (state.history.length === 0) { state.ui.histBody.innerHTML = '<tr><td colspan="5" id="histEmpty">No history yet.</td></tr>'; 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 = ` <td>${item.date}</td> <td style="font-weight:bold; color:${item.color === "White" ? "#ffffff" : "#888888"};">${item.color || "N/A"}</td> <td class="${resClass}">${item.result}</td> <td>${item.myTime} / ${item.oppTime}</td> <td class="hist-fen" data-fen="${item.fen}">${item.fen}</td> <td><button class="btn-del" data-idx="${state.history.length - 1 - index}">Delete</button></td>`; 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 ? "block" : "none"; let maxD = 18; if (settings.engineMode === "local") maxD = 25; 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 ? "block" : "none"; if (state.ui.pvGradSettings) state.ui.pvGradSettings.style.display = settings.pvCustomGradient ? "block" : "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 = `Randomized 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(); } setInterval(mainLoop, CONFIG.LOOP_MS); })();