An extremely advanced Chess.com cheat menu with two powerful Stockfish models, one backup model, and countless customization options.
// ==UserScript==
// @name BEST Cheat for Chess.com (Stockfish 18.0.0, 17.1.0 & 10.0.2, No Anti-Ban)
// @namespace http://tampermonkey.net/
// @version 8.3.6
// @description An extremely advanced Chess.com cheat menu with two powerful Stockfish models, one backup model, 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/puzzles/*
// @match https://www.chess.com/daily
// @connect chess-api.com
// @connect stockfish.online
// @grant GM_getResourceText
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @resource stockfish.js https://cdnjs.cloudflare.com/ajax/libs/stockfish.js/10.0.2/stockfish.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/m1L6LID9hJCNzb75hFL6IoD9bQcopc8SQp4ghExt9mw/pfR5APtbcwH1C68W/l8B3wO463+xAOu5gH2EkG2EENSX8F4A+wkhG7mA+l3gVwD3tBCAUvoYIeQpQkh/s2f7KaVPAthfFw/4HsA+QsjGZt/sJ5Q+01oArvN9Qkh/s2f7KaWPE0L2112Au8D3AO4jhGxs9u0+SulTAPbXFfA9gP2EkI3NvttPKX0KwP66Ar4HsJ8QsrHZd/sppU8C2F9XwPcA7ieEbGz23X5K6RMA9tcV8D2A+wkhG5t9t59S+iSA/XUFfA9gPyFkY7Pv9lNKTwLYX1fA9wD2E0I2Nvt2H6X0KQD76wr4HsB9hJCNzb7bTyl9CsD+ugK+B7CfELKx2Xf7KaUPE0L2112Au8D3AO4jhGxs9u0+SulTAPbXFfA9gP2EkI3NvttPKX0KwP66Ar4HsJ8QsrHZd/sppU8B2F9XwPcA9hNCNjb7bj+l9CkA++sK+B7AfYSQjc2+208pfQrA/rYClNI9hJCnCCHTmz3bTyl9CsD+tgOU0mcIIU8RQqY3e7afUvo0gP1tBSilz1BKnwGwv60A/H8uQAh5DsB+QsjGZt98Qil9DsD+1gKU0ucIIc8QQqY2e7afUvo8gP2tBail0N8A7iOEbGz23X5K6fMA9tcV8D2A+wkhG5t9t59S+iSA/XUFfA9gPyFkY7Pv9lNKTwLYX1fA9wDuI4RsbPbd/v8U4H/fA0II8Ty/mxDiA7C/Lh7wPID9hJCNzb7dTyl9EcD+ungA8Ty/mxDiA7C/pQCldC+l9EUA+1sK8Ty/hxDya0rpCwD2txTg/7kAIeR5APtbut8ghBBC2pZ/ALy683b5qZ2oAAAAAElFTkSuQmCC";
// --- 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,
localConfigSent: !1,
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",
// Visual Manager State
visuals: [], // { id, type('analysis'|'history'), move, interval, isFading }
};
const DEFAULT_SETTINGS = {
engineMode: "cloud",
depth: 18,
maxThinkingTime: 0,
contempt: 100,
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",
};
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;
// FIX 1: Prevent Layer Stacking (The "Dimming" Cause)
// If we are adding a HISTORY visual (finalized move), we MUST remove any existing ANALYSIS visuals.
// Otherwise, the Analysis box sits underneath the History box. When History fades, Analysis remains (looking "dim").
if (type === 'history') {
Visuals.removeByType('history');
Visuals.removeByType('analysis');
} else if (type === 'analysis') {
// If adding new analysis, clear old analysis so we don't clutter.
Visuals.removeByType('analysis');
}
// FIX 2: Deterministic IDs
// We use the move string in the ID. This guarantees we can't accidentally stack two boxes for "e2e4".
const id = `vis-${type}-${move}`;
// Check if this specific visual exists and remove it to reset the timer/animation
const existingIdx = state.visuals.findIndex(v => v.id === id);
if (existingIdx !== -1) {
Visuals.remove(id);
}
// Draw immediately
Visuals.draw(id, move);
// FIX 3: Robust Persistence Loop
// Checks every 50ms to ensure the visual stays on the board even if Chess.com refreshes the DOM.
const interval = setInterval(() => {
const vis = state.visuals.find(v => v.id === id);
// Stop if the visual was removed or is currently fading out
if (!vis || vis.isFading) {
clearInterval(interval);
return;
}
Visuals.draw(id, move);
}, 50);
const visualObj = {
id,
move,
type,
interval,
isFading: false
};
state.visuals.push(visualObj);
// Handle Timer for History moves
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;
// CHECK: Does the element already exist?
const existing = document.querySelector(`.${id}`);
if (existing) {
// FIX 4: "Live Board" Check
// If the element exists but is on a DETACHED board (old DOM from before opponent move),
// remove it so we can redraw it on the new LIVE board.
if (!state.board.contains(existing)) {
existing.remove();
} else {
return; // Exists and is valid. Do nothing.
}
}
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;
// Stop the persistence interval so it doesn't fight the fade
vis.isFading = true;
clearInterval(vis.interval);
const els = document.querySelectorAll(`.${id}`);
els.forEach(el => {
// Force CSS transition with !important to ensure it overrides any other styles
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);
}
// Remove ALL instances (cleans up any detached/ghost nodes)
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 (Fixes missing arrows) ---
const PV = {
interval: null,
lastMoves: [],
update: (pvMoves) => {
PV.lastMoves = pvMoves || [];
if (!settings.showPVArrows) {
PV.clear();
return;
}
PV.draw();
// Persistence loop (checks every 100ms)
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;
}
// Cleanup detached nodes (board refresh check)
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}`;
// If arrow exists on live board with same move, skip redraw
const el = document.querySelector(`.${id}`);
if (el && state.board.contains(el)) {
if (el.dataset.move === move) continue;
el.remove(); // Move changed, redraw
} else if (el) {
el.remove(); // Detached
}
// Calculate Color
let color = settings.highlightColor;
if (settings.pvCustomGradient) {
const start = hexToRgb(settings.pvStartColor);
const end = hexToRgb(settings.pvEndColor);
// Gradient calculation
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);
}
// Remove excess arrows if PV got shorter
let i = limit;
while (document.querySelector(`.pv-arrow-${i}`)) {
document.querySelectorAll(`.pv-arrow-${i}`).forEach(e => e.remove());
i++;
}
}
};
function drawPVArrow(move, id, color, index) {
if (!state.board) return;
let isFlipped = state.board.classList.contains("flipped");
// Fallback check for flipped board
if (!isFlipped && state.board.game && state.board.game.getPlayingAs && state.board.game.getPlayingAs() === "b") isFlipped = true;
const from = move.substring(0, 2);
const to = move.substring(2, 4);
const getCoords = (sq) => {
const file = sq.charCodeAt(0) - 97;
const rank = parseInt(sq[1]) - 1;
let x, y;
if (isFlipped) {
x = (7 - file) * 12.5 + 6.25;
y = rank * 12.5 + 6.25;
} else {
x = file * 12.5 + 6.25;
y = (7 - rank) * 12.5 + 6.25;
}
return { x, y };
};
const start = getCoords(from);
const end = getCoords(to);
// Offset start/end slightly so arrows connect nicely in a chain
// (Optional refinement: purely visual)
const dx = end.x - start.x;
const dy = end.y - start.y;
const len = Math.sqrt(dx * dx + dy * dy);
// Width based on settings or default
const width = settings.arrowWidth || 15;
const scale = width / 15; // normalize
const headLen = 4 * scale;
const headWidth = 3 * scale;
const lineWidth = 1.0 * scale;
if (len === 0) return;
// SVG Creation
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;"; // z-index lower than main move
svg.setAttribute("viewBox", "0 0 100 100");
// Arrow Math
const ux = dx / len;
const uy = dy / len;
const endLineX = end.x - ux * headLen;
const endLineY = end.y - uy * headLen;
const px = -uy;
const py = ux;
const c1X = endLineX + px * (headWidth / 2);
const c1Y = endLineY + py * (headWidth / 2);
const c2X = endLineX - px * (headWidth / 2);
const c2Y = endLineY - py * (headWidth / 2);
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} ${c1X},${c1Y} ${c2X},${c2Y}`);
poly.setAttribute("fill", color);
poly.setAttribute("fill-opacity", settings.arrowOpacity || 0.8);
svg.appendChild(line);
svg.appendChild(poly);
// Optional: Draw Number
if (settings.pvShowNumbers) {
const midX = (start.x + end.x) / 2;
const midY = (start.y + end.y) / 2;
const text = document.createElementNS(ns, "text");
text.setAttribute("x", midX);
text.setAttribute("y", midY);
text.setAttribute("dy", "0.3em"); // vertical align
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 { r, g, b } = hexToRgb(settings.highlightColor);
const color = settings.highlightColor;
const opacity = settings.arrowOpacity;
const 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);
const to = move.substring(2, 4);
const getCoords = (sq) => {
const file = sq.charCodeAt(0) - 97;
const rank = parseInt(sq[1]) - 1;
let x, y;
if (isFlipped) {
x = (7 - file) * 12.5 + 6.25;
y = rank * 12.5 + 6.25;
} else {
x = file * 12.5 + 6.25;
y = (7 - rank) * 12.5 + 6.25;
}
return { x, y };
};
const start = getCoords(from);
const end = getCoords(to);
const dx = end.x - start.x;
const dy = end.y - start.y;
const len = Math.sqrt(dx * dx + dy * dy);
const scale = width / 15;
const headLen = 4 * scale;
const headWidth = 3 * scale;
const lineWidth = 1.2 * scale;
if (len === 0) return;
const ux = dx / len;
const uy = dy / len;
const endLineX = end.x - ux * headLen;
const endLineY = end.y - uy * headLen;
const px = -uy;
const py = ux;
const corner1X = endLineX + px * (headWidth / 2);
const corner1Y = endLineY + py * (headWidth / 2);
const corner2X = endLineX - px * (headWidth / 2);
const corner2Y = endLineY - py * (headWidth / 2);
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} ${corner1X},${corner1Y} ${corner2X},${corner2Y}`);
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" };
}
// Positive Thresholds (Advantage)
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" };
// Neutral Threshold
if (relativeScore >= -0.25) return { text: "Equal", color: "#aaaaaa" };
// Negative Thresholds (Disadvantage)
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" };
// Else (Less than -3)
return { text: "Significant Disadvantage", color: "#ff0000" };
}
// --- ENGINE CORE ---
function loadLocalEngine() {
if (state.localEngine) return;
try {
const scriptContent = GM_getResourceText("stockfish.js");
if (!scriptContent) throw new Error("Stockfish resource not found.");
const blob = new Blob([scriptContent], { type: "application/javascript" });
state.localEngine = new Worker(URL.createObjectURL(blob));
state.localEngine.onmessage = handleLocalMessage;
state.localEngine.onerror = (e) => handleError("Local Engine Error", e);
[
"ucinewgame",
"isready",
"setoption name MultiPV value 1",
`setoption name Contempt value ${settings.contempt}`,
].forEach((c) => state.localEngine.postMessage(c));
console.log("Stockfish 10 Local Loaded.");
} catch (e) {
handleError("Engine Load Fail", e);
}
}
function triggerFallback() {
if (settings.engineMode === 'local') return;
console.warn("API Error detected. Switching to Local Stockfish 10 at Depth 12.");
settings.engineMode = 'local';
settings.depth = 12;
saveSetting('engineMode', 'local');
saveSetting('depth', 12);
if(state.ui.selMode) state.ui.selMode.value = 'local';
if(state.ui.inpDepth) state.ui.inpDepth.value = 12;
state.lastMoveResult = "⚠️ API Error. Switched to Local.";
loadLocalEngine();
if(state.lastSanitizedBoardFEN) {
analyzeLocal(state.lastSanitizedBoardFEN, 12);
}
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;
const 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..." : "☁️ SF17 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 encodedFEN = encodeURIComponent(finalFEN);
const url = `https://stockfish.online/api/s/v2.php?fen=${encodedFEN}&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: 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();
if (!state.localEngine) return;
if (!state.localConfigSent) {
state.localEngine.postMessage(`setoption name Contempt value ${settings.contempt}`);
state.localConfigSent = !0;
}
state.localEval = null; state.localMate = null; state.localPV = null; state.localDepth = null;
const actualDepth = Math.min(depth, 23);
const cmds = [`position fen ${fen}`, `go depth ${actualDepth}`];
state.lastPayload = `Worker CMDs:\n${cmds.join("\n")}`;
state.ui.liveOutput.innerHTML = "⚡ Local Analysis...";
updateUI();
cmds.forEach((cmd) => state.localEngine.postMessage(cmd));
}
function handleLocalMessage(e) {
const msg = e.data;
if (typeof msg !== "string") return;
state.lastResponse = (state.lastResponse.length > 500 ? "..." + state.lastResponse.slice(-500) : state.lastResponse) + "\n" + msg;
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(" ");
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');
// --- TRIGGER PV UPDATE ---
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');
// Clear PV when move is final (optional, but cleaner)
PV.clear();
} else {
Visuals.add(bestMove, 'analysis');
// --- UPDATE PV DISPLAY ---
PV.update(state.currentPV);
// ------------------------
}
let scoreTxt = "";
let pvStr = "N/A";
let numericValForStatus = 0;
let 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);
const 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((mutations) => {
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) {
if (!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);
}
// Force redraw of current history visuals to update color
Visuals.removeByType('history'); // Clear old colors
if(state.currentBestMove) Visuals.add(state.currentBestMove, 'history');
}
function applyTheme() {
const modals = [state.ui.panel, state.ui.modal, state.ui.histModal];
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);
// This forces the "Recording Enabled" label and category text to update
m.style.color = settings.themeText;
// Apply menu opacity to the background only, keeping text sharp
if (m === state.ui.panel) {
m.style.opacity = settings.menuOpacity;
} else {
// For the pop-up modals
const overlayId = m.id === "modal" ? "modalOv" : "histModalOv";
const overlay = document.getElementById(overlayId);
if (overlay) overlay.style.opacity = "1";
m.style.opacity = settings.menuOpacity;
}
});
}
// --- POSITION LOGIC ---
function applyMenuPosition() {
const p = state.ui.panel;
if(!p) return;
const margin = "10px";
p.style.transform = "none";
// Important: Clear conflicting properties
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";
// Bounds check to ensure it's not off-screen
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 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("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 style = `
:root { --bot-bg:${settings.themeBg}; --bot-b:${settings.themeBorder}; --bot-p:${settings.themePrimary}; --bot-t:${settings.themeText}; --bot-inp:#333; }
#enginePanel * { box-sizing: border-box; }
#enginePanel {
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};
}
#enginePanel.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;
}
#enginePanel.minified #panelContent,
#enginePanel.minified #panelHeader > *:not(#minBtn) { display: none !important; }
#enginePanel.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;
}
#enginePanel.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; }
.sect { border-top:1px solid #333; padding-top:10px; display:flex; flex-direction:column; gap:8px; }
.sect-title { font-size:0.85em; color:#aaa; font-weight:bold; text-transform:uppercase; margin-bottom:4px; }
.row { display:flex; justify-content:space-between; align-items:center; gap: 10px; margin-bottom: 6px; }
input, select { background:rgba(0,0,0,0.2); color:var(--bot-t); border:1px solid var(--bot-b); padding:4px; border-radius:4px; }
input[type="number"] { width: 60px; }
select { width: 120px; }
input[type="text"] { flex:1; }
/* --- UPDATED SLIDER STYLING --- */
input[type=range] {
-webkit-appearance: none;
width: 100%;
background: transparent;
padding: 0;
margin: 0;
border: none;
}
input[type=range]:focus { outline: none; }
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 6px;
cursor: pointer;
background: var(--bot-b); /* Track matches border color */
border-radius: 3px;
}
input[type=range]::-webkit-slider-thumb {
height: 16px;
width: 16px;
border-radius: 50%;
background: var(--bot-t); /* Thumb matches Text Color (White in Dark mode, Black in Light) */
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);
}
/* Rainbow Hue Slider Override */
#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;
}
/* --------------------------- */
button { background:var(--bot-p); border:none; padding:10px; color:#000; font-weight:bold; cursor:pointer; border-radius:4px; }
button:disabled { opacity:0.6; cursor:not-allowed; }
#custBtn { background:#00bcd4; margin-top:5px; }
#histBtn { background:#8e44ad; margin-top:5px; color: white; }
.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; }
#modalOv, #histModalOv { 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; }
#modal, #histModal { background:var(--bot-bg); padding:0; border-radius:8px; width:420px; border:1px solid var(--bot-b); display:flex; flex-direction:column; max-height:90vh; opacity: ${settings.menuOpacity}; }
#histModal { width: 600px; height: 600px; }
#modal * { color:var(--bot-t); }
#modal label, #histModal label { opacity: 1 !important; font-weight: 600; font-size:0.9em; }
#modal input[type="color"] { height: 24px; padding: 0; width: 40px; cursor:pointer; border: none; }
#modal select { height: 24px; padding: 0 4px; font-size: 0.9em; }
.show-cloud { display: none; } .show-local { display: none; }
body.mode-cloud .show-cloud { display: flex; }
body.mode-local .show-local { display: flex; }
.rgb-inputs { display: flex; gap: 5px; flex: 1; justify-content: flex-end; }
.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; }
/* Replace the #histTable th and td sections with this: */
#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); /* This ensures text swaps with theme */}
#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; color: white; 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; }
/* Visual Modal Specifics */
.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); }
.tab-btn { flex:1; background:transparent; border:none; padding:10px; color:var(--bot-t); cursor:pointer; opacity:0.7; border-bottom:2px solid transparent; }
.tab-btn.active { opacity:1; border-bottom:2px solid var(--bot-p); font-weight:bold; }
.modal-content { padding:15px; overflow-y:auto; flex:1; }
.slider-group { display: flex; align-items: center; gap: 8px; flex: 1; justify-content: flex-end; }
.slider-group input[type=range] { flex: 1; }
.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; }
.modal-content .row { display: flex; align-items: center; margin-bottom: 12px; }
.modal-content .row label { flex: 0 0 120px; text-align:left; font-weight: 600; }
.modal-content .row > input[type="text"], .modal-content .row > input[type="color"], .modal-content .row > select { flex: 1; }
.adv-sect { display: none; margin-top: 10px; padding-left: 10px; border-left: 2px solid var(--bot-b); display:flex; flex-direction:column; gap:8px; display:none; }
.theme-presets { display:flex; gap:10px; margin-bottom:10px; }
.theme-btn { flex:1; padding:5px; border:1px solid var(--bot-b); cursor:pointer; background:rgba(0,0,0,0.2); color:var(--bot-t); }
`;
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 10.0.2(local 0.00-75.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-local"><label>Contempt (-100→100)</label><input type="number" id="inpContempt" min="-100" max="100" value="${settings.contempt}"></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>
<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;">
<!-- BOXES -->
<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>
<!-- ARROWS -->
<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>
<!-- OUTLINE -->
<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; padding:5px 10px; color:white; font-size:0.8em;">Delete All</button>
</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"),
inpContempt: document.getElementById("inpContempt"),
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"),
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"),
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();
// 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(); } };
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
) {
// Force refresh for history visuals if they exist
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;
// Force custom mode on drag start
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;
let y = mv.clientY - startY;
// Bounds
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(); };
// Duration Slider Logic
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;
let secs = 59.9 * Math.pow((val - 1) / 98, 2) + 0.1;
return Math.round(secs * 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();
// Tab & Colors
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();
};
// Standard Bindings
bind(state.ui.inpDepth, "depth", "num");
bind(state.ui.inpTime, "maxThinkingTime", "num");
bind(state.ui.inpContempt, "contempt", "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");
// Color Sliders
[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("/");
let board = [];
for (let r of rows) {
let rowArr = [];
for (let char of r) {
if (!isNaN(char)) {
let empties = parseInt(char);
for (let k = 0; k < empties; 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];
const 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>`;
}
}
html += "</div>";
return html;
}
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;
}
const sorted = [...state.history].reverse();
sorted.forEach((item, index) => {
const tr = document.createElement("tr");
let resClass = "hist-draw";
if (item.result === "Win") resClass = "hist-win";
else if (item.result === "Loss") resClass = "hist-loss";
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;
let 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) => {
return row.split("").reverse().join("");
})
.join("/");
fen = parts.join(" ");
}
}
let myTime = "N/A";
let oppTime = "N/A";
const clockBot = document.querySelector(".clock-bottom .clock-time-monospace, .clock-bottom");
const clockTop = document.querySelector(".clock-top .clock-time-monospace, .clock-top");
if (clockBot) myTime = clockBot.innerText;
if (clockTop) oppTime = clockTop.innerText;
let resultTxt = "Ended";
let simpleRes = "Draw";
const mainMsg = resultEl.querySelector(".game-result-main-message, .game-over-header-title");
if (mainMsg) resultTxt = mainMsg.innerText.trim();
else resultTxt = resultEl.innerText.split("\n")[0].trim();
const subMsgEl = resultEl.querySelector(".game-result-sub-message, .game-over-header-subtitle");
let subMsg = subMsgEl ? subMsgEl.innerText.trim() : "";
const fullText = (resultTxt + " " + subMsg).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";
}
const gameObj = {
date: new Date().toLocaleString(),
color: playerColor,
result: simpleRes,
fen: fen,
myTime: myTime,
oppTime: oppTime,
id: Date.now(),
};
state.history.push(gameObj);
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();
const winW = window.innerWidth;
const winH = window.innerHeight;
if (rect.right > winW) state.ui.panel.style.width = winW - rect.left + "px";
if (rect.bottom > winH) state.ui.panel.style.height = winH - 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 = 23;
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() {
if (state.board?.game && settings.autoRun) {
const raw = getRawBoardFEN();
if (raw) {
const clean = sanitizeFEN(raw);
// ── NEW: Hard-shutoff all visuals the instant any move is played ──
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);
}
}
}
state.board = document.querySelector(CONFIG.BOARD_SEL);
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);
})();