// ==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/stockfish@18.0.5/bin/stockfish-18-single.js
// @run-at document-idle
// @downloadURL none
// ==/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/stockfish@18.0.5/bin/stockfish-18-single.wasm";
const WASM_PRESETS = [
{ label: "SF 18.0.5 Single-thread (unpkg)", url: "https://unpkg.com/stockfish@18.0.5/bin/stockfish-18-single.wasm" },
{ label: "SF 18.0.5 Single-thread (jsDelivr)", url: "https://cdn.jsdelivr.net/npm/stockfish@18.0.5/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 = `
`;
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 {
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}: ${best}`;
}
state.lastLiveResult = `
${scoreTxt}
${statusData.text}
(${duration}s)
`;
updateUI();
}
}
if (msg.startsWith("bestmove")) {
state.isThinking = !1;
const parts = msg.split(" ");
const bestMove = parts[1];
if (bestMove && bestMove !== "(none)") {
const duration = ((performance.now() - state.analysisStartTime) / 1000).toFixed(2);
processBestMove(bestMove, state.localEval, state.localMate, state.localPV ? state.localPV.split(" ") : null, null, duration, state.localDepth, true);
} else state.lastMoveResult = "⚠️ No move found";
updateUI();
}
}
function processBestMove(bestMove, evalScore, mate, continuationArr, winChance, duration, depth = null, isFinal = false) {
state.currentBestMove = bestMove;
state.currentPV = continuationArr || (bestMove ? [bestMove] : []);
if (isFinal || !state.isThinking) { Visuals.add(bestMove, 'history'); PV.clear(); }
else { Visuals.add(bestMove, 'analysis'); PV.update(state.currentPV); }
const evalNum = (evalScore !== null && evalScore !== undefined) ? parseFloat(evalScore) : null;
const mateNum = (mate !== null && mate !== undefined && mate !== 0) ? parseInt(mate) : null;
EvalBar.update(evalNum, mateNum);
let scoreTxt = "", pvStr = "N/A", numericValForStatus = 0, isMate = false;
if (evalScore !== undefined || mate !== undefined) {
if (mate) {
isMate = true; numericValForStatus = mate;
scoreTxt = `M${Math.abs(mate)}`; if (mate < 0) scoreTxt = "-" + scoreTxt;
} else {
const sc = parseFloat(evalScore); numericValForStatus = sc;
scoreTxt = (sc > 0 ? "+" : "") + sc;
}
if (continuationArr) pvStr = continuationArr.join(" ");
}
const statusData = getEvalStatusData(numericValForStatus, isMate);
const durHtml = duration ? `(${duration}s)` : "";
state.lastMoveResult = `✅ Best: ${bestMove}`;
let wcHtml = "";
if (winChance) wcHtml = `(${Math.round(winChance)}%)`;
else if (depth) wcHtml = `(D${depth})`;
state.lastLiveResult = `
${scoreTxt}
${statusData.text}
${wcHtml} ${durHtml}
PV: ${pvStr}
`;
if (settings.autoMove) triggerAutoMove();
}
function triggerAutoMove() {
if (!state.currentBestMove || !state.board?.game) return;
const turn = state.board.game.getTurn();
const playingAs = state.board.game.getPlayingAs();
if (turn !== playingAs) return;
const wait = Math.max(0, state.moveTargetTime - performance.now());
setTimeout(() => playMove(state.currentBestMove), wait);
}
function handleError(type, err) {
state.isThinking = !1;
console.error(type, err);
state.lastResponse = `${type}: ${err?.message || err}`;
state.lastMoveResult = `❌ ${type}`;
updateUI();
}
function playMove(move) {
if (!state.board?.game) return;
const from = move.substring(0, 2), to = move.substring(2, 4);
const currentRaw = getRawBoardFEN();
if (currentRaw && sanitizeFEN(currentRaw).split(" ")[0] !== state.lastSentFEN.split(" ")[0]) return;
for (const m of state.board.game.getLegalMoves()) {
if (m.from === from && m.to === to) {
const promotion = move.length > 4 ? move.substring(4, 5) : "q";
state.board.game.move({ ...m, promotion, animate: !0, userGenerated: !0 });
return;
}
}
}
function toggleAutoQueue() {
if (state.newGameObserver) { state.newGameObserver.disconnect(); state.newGameObserver = null; }
if (state.queueTimeout) { clearTimeout(state.queueTimeout); state.queueTimeout = null; }
if (settings.autoQueue) {
state.newGameObserver = new MutationObserver(() => {
const btns = Array.from(document.querySelectorAll("button"));
const newGameBtn = btns.find((b) => {
const txt = b.innerText.toLowerCase();
return txt.includes("new") && !txt.includes("rematch") && b.offsetParent !== null;
});
if (newGameBtn && !state.queueTimeout) {
state.queueTimeout = setTimeout(() => { newGameBtn.click(); state.queueTimeout = null; }, 100);
}
});
state.newGameObserver.observe(document.body, { childList: !0, subtree: !0 });
}
}
function resetSettings() {
const currentModel = settings.engineMode;
Object.assign(settings, DEFAULT_SETTINGS);
settings.engineMode = currentModel;
Object.keys(DEFAULT_SETTINGS).forEach((k) => { if (k !== "engineMode") saveSetting(k, DEFAULT_SETTINGS[k]); });
saveSetting("engineMode", currentModel);
const hsl = rgbToHsl(...Object.values(hexToRgb(settings.highlightColor)));
state.h = hsl.h; state.s = hsl.s; state.l = hsl.l;
toggleAutoQueue();
createUI();
applyMenuPosition();
}
function syncColor() {
const rgb = hslToRgb(state.h, state.s, state.l);
const hex = rgbToHex(rgb.r, rgb.g, rgb.b);
settings.highlightColor = hex;
saveSetting("highlightColor", hex);
if (state.ui.inpR) {
state.ui.inpR.value = rgb.r; state.ui.inpG.value = rgb.g; state.ui.inpB.value = rgb.b;
state.ui.inpHex.value = hex;
state.ui.colorPreview.style.background = hex;
state.ui.sliderH.value = state.h; state.ui.sliderS.value = state.s; state.ui.sliderL.value = state.l;
if (state.ui.sliderHNum) state.ui.sliderHNum.value = Math.round(state.h);
if (state.ui.sliderSNum) state.ui.sliderSNum.value = Math.round(state.s);
if (state.ui.sliderLNum) state.ui.sliderLNum.value = Math.round(state.l);
}
Visuals.removeByType('history');
if (state.currentBestMove) Visuals.add(state.currentBestMove, 'history');
}
function applyTheme() {
const modals = [state.ui.panel, state.ui.modal, state.ui.histModal, state.ui.localModal];
modals.forEach(m => {
if (!m) return;
m.style.setProperty("--bot-bg", settings.themeBg);
m.style.setProperty("--bot-t", settings.themeText);
m.style.setProperty("--bot-b", settings.themeBorder);
m.style.setProperty("--bot-p", settings.themePrimary);
m.style.color = settings.themeText;
if (m === state.ui.panel) {
m.style.opacity = settings.menuOpacity;
} else {
const overlayId = m.id === "modal" ? "modalOv" : m.id === "histModal" ? "histModalOv" : "localModalOv";
const overlay = document.getElementById(overlayId);
if (overlay) overlay.style.opacity = "1";
m.style.opacity = settings.menuOpacity;
}
});
}
function applyMenuPosition() {
const p = state.ui.panel;
if (!p) return;
const margin = "10px";
p.style.transform = "none";
p.style.top = ""; p.style.bottom = ""; p.style.left = ""; p.style.right = "";
if (settings.menuPosition === "custom") {
const savedX = GM_getValue("bot_pX", "auto");
const savedY = GM_getValue("bot_pY", "0");
if (savedX === "auto") { p.style.right = "0px"; p.style.left = "auto"; }
else p.style.left = savedX + "px";
p.style.top = savedY + "px";
const rect = p.getBoundingClientRect();
if (rect.left < 0) p.style.left = "0px";
if (rect.top < 0) p.style.top = "0px";
if (rect.right > window.innerWidth) p.style.left = (window.innerWidth - rect.width) + "px";
if (rect.bottom > window.innerHeight) p.style.top = (window.innerHeight - rect.height) + "px";
} else {
switch (settings.menuPosition) {
case "top-left": p.style.top = margin; p.style.left = margin; break;
case "top-right": p.style.top = margin; p.style.right = margin; break;
case "bottom-left": p.style.bottom = margin; p.style.left = margin; break;
case "bottom-right": p.style.bottom = margin; p.style.right = margin; break;
}
}
}
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 = `` + `
${state.lastLiveResult}
${state.lastMoveResult}
▼ Advanced Visual Settings
Menu Position
Engine Status
❌ Not Installed
WASM Source
WASM is cached in IndexedDB after first download. Use Reinstall to apply a new URL.
JS Source (from @resource)
https://unpkg.com/stockfish@18.0.5/bin/stockfish-18-single.js
Fixed at install time. Change in @resource header to update.
`;
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 ? `
` : "▼";
};
state.ui.minBtn.onclick = (e) => { e.stopPropagation(); toggleMin(); };
state.ui.panel.onclick = (e) => { if (state.ui.panel.classList.contains("minified")) toggleMin(); };
if (isMini) state.ui.minBtn.innerHTML = `
`;
const bind = (el, key, type = "val") => {
if (!el) return;
el.addEventListener(type === "chk" ? "change" : "input", (e) => {
const val = type === "chk" ? e.target.checked : type === "num" ? parseFloat(e.target.value) : e.target.value;
saveSetting(key, val);
if (key === "autoMove" && val === !0) triggerAutoMove();
if (key === "autoQueue") toggleAutoQueue();
if (key === "hideAfterMove" && val === !0) { Visuals.removeByType('history'); Visuals.removeByType('analysis'); PV.clear(); }
if (["innerOpacity","outerOpacity","gradientBias","arrowOpacity","arrowWidth","visualOutlineWidth","visualOutlineOpacity","visualOutlineGlow","visualOutlineGlowRadius"].includes(key) && state.currentBestMove) {
Visuals.removeByType('history');
Visuals.add(state.currentBestMove, 'history');
}
if (["themeBg","themeText","themeBorder","themePrimary","menuOpacity"].includes(key)) applyTheme();
updateUI();
});
};
const bindSlider = (rangeEl, numEl, key, isPct = false) => {
if (!rangeEl || !numEl) return;
rangeEl.oninput = () => {
let val = parseFloat(rangeEl.value);
saveSetting(key, val);
numEl.value = isPct ? Math.round(val * 100) : val;
if (key === "menuOpacity") applyTheme();
if (state.currentBestMove) { Visuals.removeByType('history'); Visuals.add(state.currentBestMove, 'history'); }
};
numEl.oninput = () => {
let val = parseFloat(numEl.value);
if (isPct) val /= 100;
saveSetting(key, val);
rangeEl.value = val;
if (key === "menuOpacity") applyTheme();
if (state.currentBestMove) { Visuals.removeByType('history'); Visuals.add(state.currentBestMove, 'history'); }
};
};
state.ui.selMenuPos.onchange = (e) => { saveSetting("menuPosition", e.target.value); applyMenuPosition(); };
state.ui.header.onmousedown = (e) => {
if (e.target.id === "minBtn" || e.target.id === "btnReset") return;
if (state.ui.panel.classList.contains("minified")) return;
if (settings.menuPosition !== 'custom') { saveSetting("menuPosition", 'custom'); state.ui.selMenuPos.value = 'custom'; }
e.preventDefault();
const startX = e.clientX - state.ui.panel.offsetLeft;
const startY = e.clientY - state.ui.panel.offsetTop;
const onMove = (mv) => {
let x = mv.clientX - startX, y = mv.clientY - startY;
x = Math.max(0, Math.min(x, window.innerWidth - state.ui.panel.offsetWidth));
y = Math.max(0, Math.min(y, window.innerHeight - state.ui.panel.offsetHeight));
state.ui.panel.style.left = x + "px"; state.ui.panel.style.top = y + "px";
state.ui.panel.style.right = "auto"; state.ui.panel.style.bottom = "auto";
saveSetting("pX", x); saveSetting("pY", y);
};
document.addEventListener("mousemove", onMove);
document.onmouseup = () => document.removeEventListener("mousemove", onMove);
};
new ResizeObserver(() => {
if (!state.ui.panel.classList.contains("minified")) {
saveSetting("panelW", state.ui.panel.style.width);
saveSetting("panelH", state.ui.panel.style.height);
}
}).observe(state.ui.panel);
state.ui.selMode.onchange = (e) => { saveSetting("engineMode", e.target.value); state.isThinking = !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 = '';
for (let r = 0; r < 8; r++) for (let c = 0; c < 8; c++) {
const piece = board[r][c], isDark = (r+c)%2===1;
const bg = piece ? `style="background-image: url('${PIECE_IMGS[piece]}');"` : "";
html += `
`;
}
return html + "
";
}
function renderHistory() {
if (!state.ui.histBody) return;
state.ui.histBody.innerHTML = "";
if (state.history.length === 0) { state.ui.histBody.innerHTML = '| No history yet. |
'; return; }
[...state.history].reverse().forEach((item, index) => {
const tr = document.createElement("tr");
let resClass = item.result === "Win" ? "hist-win" : item.result === "Loss" ? "hist-loss" : "hist-draw";
tr.innerHTML = `
${item.date} |
${item.color || "N/A"} |
${item.result} |
${item.myTime} / ${item.oppTime} |
${item.fen} |
| `;
state.ui.histBody.appendChild(tr);
});
document.querySelectorAll(".btn-del").forEach((btn) => {
btn.onclick = (e) => { const idx = parseInt(e.target.dataset.idx); state.history.splice(idx, 1); GM_setValue("bot_history", state.history); renderHistory(); };
});
document.querySelectorAll(".hist-fen").forEach((el) => {
el.onmouseenter = (e) => {
const fen = e.target.getAttribute("data-fen");
if (fen && state.ui.fenTooltip) {
state.ui.fenTooltip.innerHTML = drawFenBoard(fen);
state.ui.fenTooltip.style.display = "block";
const rect = e.target.getBoundingClientRect();
let left = rect.left + 20, top = rect.bottom + 5;
if (left + 250 > window.innerWidth) left = window.innerWidth - 260;
if (top + 250 > window.innerHeight) top = rect.top - 260;
state.ui.fenTooltip.style.left = left + "px"; state.ui.fenTooltip.style.top = top + "px";
}
};
el.onmouseleave = () => { if (state.ui.fenTooltip) state.ui.fenTooltip.style.display = "none"; };
});
}
function checkForGameOver() {
if (!settings.enableHistory) return;
const resultEl = document.querySelector(".game-result-component, .game-over-modal-content, .daily-game-footer-game-over");
if (resultEl) {
if (state.hasSavedCurrentGameResult) return;
let fen = sanitizeFEN(getRawBoardFEN());
let playingAsCode = state.playingAs;
if (!playingAsCode && state.board?.game?.getPlayingAs) { try { playingAsCode = state.board.game.getPlayingAs(); } catch (e) {} }
if (playingAsCode !== 1 && playingAsCode !== 2) playingAsCode = 0;
const playerColor = playingAsCode === 2 ? "Black" : "White";
if (playingAsCode === 2) {
let parts = fen.split(" ");
if (parts.length > 0) { parts[0] = parts[0].split("/").reverse().map((row) => row.split("").reverse().join("")).join("/"); fen = parts.join(" "); }
}
const clockBot = document.querySelector(".clock-bottom .clock-time-monospace, .clock-bottom");
const clockTop = document.querySelector(".clock-top .clock-time-monospace, .clock-top");
let myTime = clockBot ? clockBot.innerText : "N/A";
let oppTime = clockTop ? clockTop.innerText : "N/A";
let simpleRes = "Draw";
const mainMsg = resultEl.querySelector(".game-result-main-message, .game-over-header-title");
const subMsgEl = resultEl.querySelector(".game-result-sub-message, .game-over-header-subtitle");
const fullText = ((mainMsg ? mainMsg.innerText : resultEl.innerText.split("\n")[0]) + " " + (subMsgEl ? subMsgEl.innerText : "")).toLowerCase();
if (resultEl.classList.contains("game-result-win")) simpleRes = "Win";
else if (resultEl.classList.contains("game-result-loss")) simpleRes = "Loss";
else if (resultEl.classList.contains("game-result-draw")) simpleRes = "Draw";
else if (fullText.includes("you won")) simpleRes = "Win";
else if (fullText.includes("you lost")) simpleRes = "Loss";
else if (playingAsCode === 1 && fullText.includes("white won")) simpleRes = "Win";
else if (playingAsCode === 1 && fullText.includes("black won")) simpleRes = "Loss";
else if (playingAsCode === 2 && fullText.includes("black won")) simpleRes = "Win";
else if (playingAsCode === 2 && fullText.includes("white won")) simpleRes = "Loss";
state.history.push({ date: new Date().toLocaleString(), color: playerColor, result: simpleRes, fen, myTime, oppTime, id: Date.now() });
if (state.history.length > 200) state.history.shift();
GM_setValue("bot_history", state.history);
state.hasSavedCurrentGameResult = !0;
if (state.ui.histModal && state.ui.histModal.style.display !== "none") renderHistory();
} else {
state.hasSavedCurrentGameResult = !1;
}
}
function enforceBounds() {
if (state.ui.panel) {
const rect = state.ui.panel.getBoundingClientRect();
if (rect.right > window.innerWidth) state.ui.panel.style.width = window.innerWidth - rect.left + "px";
if (rect.bottom > window.innerHeight) state.ui.panel.style.height = window.innerHeight - rect.top + "px";
if (rect.left < 0) state.ui.panel.style.left = "0px";
if (rect.top < 0) state.ui.panel.style.top = "0px";
}
requestAnimationFrame(enforceBounds);
}
requestAnimationFrame(enforceBounds);
function updateUI() {
if (!state.ui.panel) return;
document.body.classList.remove("mode-cloud", "mode-local", "mode-sfonline");
document.body.classList.add(`mode-${settings.engineMode}`);
if (state.ui.debugArea) state.ui.debugArea.style.display = settings.debugLogs ? "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);
})();