// ==UserScript==
// @name Bintang Toba Pro
// @namespace Bintang-Toba-Pro
// @version 1.0.0
// @description sistem string concatenation style - type scrip ES6
// @author Delta-Polder-Indonesia
// @license GPL-3.0-only
// @match https://www.chess.com/*
// @icon https://cdn.corenexis.com/files/c/3853186720.png
// @run-at document-idle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_getResourceText
// @grant GM_registerMenuCommand
// @grant GM_info
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @resource stockfishjs https://cdnjs.cloudflare.com/ajax/libs/stockfish.js/10.0.2/stockfish.js
// @resource openingbook https://raw.githubusercontent.com/JD-YH03D/release/refs/heads/main/Chess.com%20-%20Play%20Chess%20Online%20-%20Free%20Games/OpeningBook.json
// @connect localhost
// @connect cdnjs.cloudflare.com
// @connect unpkg.com
// @connect jsdelivr.net
// @connect raw.githubusercontent.com
// @connect drive.google.com
// @antifeature none
// ==/UserScript==
(function () {
"use strict";
// =====================================================
// Section 01: Enhanced Tampermonkey Polyfills
// =====================================================
let isTampermonkey = typeof GM_getValue === "function" && typeof GM_xmlhttpRequest === "function";
if (!isTampermonkey) {
window.GM_getValue = (key, defaultValue) => {
try {
const item = localStorage.getItem("tm_" + btoa(key));
if (item === null) return defaultValue;
let decoded;
try {
decoded = atob(item);
return JSON.parse(decoded);
} catch (e2) {
return decoded !== undefined ? decoded : defaultValue;
}
} catch (e) {
return defaultValue;
}
};
window.GM_setValue = (key, value) => {
try {
const str = typeof value === "string" ? value : JSON.stringify(value);
localStorage.setItem("tm_" + btoa(key), btoa(str));
} catch (e) { }
};
window.GM_getResourceText = (name) => {
return "";
};
window.GM_registerMenuCommand = (name, fn) => {
log("Registering menu command:", name);
window["tmCommand_" + name.replace(/\s+/g, "_")] = fn;
};
window.GM_info = {
script: {
name: "Bintang Toba Pro",
version: "1.0.0",
namespace: "Bintang-Toba-Pro"
}
};
window.GM_xmlhttpRequest = (details) => {
const xhr = new XMLHttpRequest();
const timeout = details.timeout || 10000;
const retries = details.retries || 0;
let attempt = 0;
function doRequest() {
xhr.open(details.method || "GET", details.url, true);
xhr.timeout = timeout;
if (details.headers) {
Object.keys(details.headers).forEach((k) => {
xhr.setRequestHeader(k, details.headers[k]);
});
}
xhr.onload = () => {
if (details.onload) {
details.onload({
status: xhr.status,
responseText: xhr.responseText,
finalUrl: xhr.responseURL || details.url
});
}
};
xhr.onerror = (e) => {
if (attempt < retries) {
attempt++;
scheduleManagedTimeout(doRequest, 1000 * attempt);
} else if (details.onerror) {
details.onerror(e);
}
};
xhr.ontimeout = (e) => {
if (details.ontimeout) details.ontimeout(e);
else if (details.onerror) details.onerror(e);
};
xhr.send(details.data || null);
}
doRequest();
};
window.GM_addStyle = (css) => {
const style = document.createElement('style');
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
return style;
};
if (typeof unsafeWindow === "undefined") {
try {
if (typeof window.unsafeWindow === "undefined") {
Object.defineProperty(window, 'unsafeWindow', {
value: window.wrappedJSObject || window,
writable: true,
enumerable: false,
configurable: true
});
}
} catch (e) {
if (typeof window.unsafeWindow === "undefined") {
try { window.unsafeWindow = window; } catch (err) { }
}
}
}
}
// =====================================================
// Section 02: Multi-Source Stockfish Loader
// =====================================================
let EngineLoader = {
sources: [
{ url: "https://cdnjs.cloudflare.com/ajax/libs/stockfish.js/10.0.2/stockfish.js", weight: 1 },
{ url: "https://unpkg.com/stockfish.js@10.0.2/stockfish.js", weight: 1 },
{ url: "https://cdn.jsdelivr.net/npm/stockfish.js@10.0.2/stockfish.js", weight: 1 }
],
stockfishSourceCode: "",
currentSourceIndex: 0,
loadWithFallback: function () {
let self = this;
return new Promise(function (resolve, reject) {
function tryNextSource() {
if (self.currentSourceIndex >= self.sources.length) {
reject(new Error("All Stockfish sources failed"));
return;
}
let source = self.sources[self.currentSourceIndex++];
self.loadFromURL(source.url)
.then(function (code) {
self.stockfishSourceCode = code;
log("Stockfish loaded from:", source.url, "Size:", code.length);
resolve(true);
})
.catch(function (e) {
warn("Source failed:", source.url, e);
tryNextSource();
});
}
tryNextSource();
});
},
loadFromURL: function (url) {
return new Promise(function (resolve, reject) {
let xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.timeout = 15000;
xhr.onload = function () {
if (xhr.status === 200 && xhr.responseText.length > 50000) {
resolve(xhr.responseText);
} else {
reject(new Error("Invalid response"));
}
};
xhr.onerror = function () { reject(new Error("Network error")); };
xhr.ontimeout = function () { reject(new Error("Timeout")); };
xhr.send();
});
},
loadAsync: function (onProgress) {
let self = this;
return new Promise(function (resolve, reject) {
if (self.stockfishSourceCode && self.stockfishSourceCode.length > 50000) {
resolve(true);
return;
}
try {
let resource = GM_getResourceText("stockfishjs");
if (resource && resource.length > 50000) {
self.stockfishSourceCode = resource;
resolve(true);
return;
}
} catch (e) { }
self.loadWithFallback().then(resolve).catch(reject);
});
}
};
// =====================================================
// Section 03: DOM Utility Functions
// =====================================================
function $(sel, root) {
return (root || document).querySelector(sel);
}
function $$(sel, root) {
return Array.from((root || document).querySelectorAll(sel));
}
function sleep(ms) {
return new Promise(function (resolve) {
scheduleManagedTimeout(resolve, ms);
});
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
function escapeHtml(text) {
if (typeof text !== 'string') return '';
return text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
}
// =====================================================
// Section 04: Debug and Error Logging
// =====================================================
const DEBUG = true;
function isSilentLoggingEnabled() {
try {
if (typeof State !== "undefined" && State && typeof State.silentLogging === "boolean") {
return !!State.silentLogging;
}
return !!GM_getValue("silentLogging", false);
} catch (e) {
return false;
}
}
const ErrorTelemetry = {
moduleCounts: {
engine: 0,
ui: 0,
premove: 0,
syzygy: 0,
runtime: 0,
other: 0
},
recent: [],
maxRecent: 20
};
function _inferErrorModule(args) {
let text = args.map(function (x) {
if (x === null || x === undefined) return "";
if (typeof x === "string") return x;
try {
return JSON.stringify(x);
} catch (e) {
return String(x);
}
}).join(" ").toLowerCase();
if (text.includes("syzygy")) return "syzygy";
if (text.includes("premove")) return "premove";
if (text.includes("engine") || text.includes("stockfish") || text.includes("worker")) return "engine";
if (text.includes("ui") || text.includes("panel") || text.includes("render")) return "ui";
if (text.includes("watchdog") || text.includes("loop") || text.includes("runtime")) return "runtime";
return "other";
}
function reportErrorTelemetry(args) {
try {
let module = _inferErrorModule(args);
if (!Object.prototype.hasOwnProperty.call(ErrorTelemetry.moduleCounts, module)) module = "other";
ErrorTelemetry.moduleCounts[module]++;
let msg = args.map(function (x) {
if (x === null || x === undefined) return "";
if (typeof x === "string") return x;
if (x instanceof Error) return x.message || String(x);
try {
return JSON.stringify(x);
} catch (e) {
return String(x);
}
}).join(" ").trim();
ErrorTelemetry.recent.push({
ts: Date.now(),
module: module,
message: msg || "Unknown error"
});
if (ErrorTelemetry.recent.length > ErrorTelemetry.maxRecent) {
ErrorTelemetry.recent = ErrorTelemetry.recent.slice(-ErrorTelemetry.maxRecent);
}
} catch (e) {
}
}
function log() {
if (!DEBUG || isSilentLoggingEnabled()) return;
console.log.apply(console, ["[ChessAssistant]"].concat([...arguments]));
}
function warn() {
if (!DEBUG || isSilentLoggingEnabled()) return;
console.warn.apply(console, ["[ChessAssistant]"].concat([...arguments]));
}
function err() {
reportErrorTelemetry(Array.from(arguments));
if (isSilentLoggingEnabled()) return;
console.error.apply(console, ["[ChessAssistant]"].concat([...arguments]));
}
window.addEventListener("unhandledrejection", function (e) {
let reason = e && e.reason ? e.reason : "Unhandled promise rejection";
let reasonText = "";
try {
reasonText = typeof reason === "string" ? reason : (reason && reason.message ? reason.message : String(reason));
} catch (e2) {
reasonText = "";
}
if (reason instanceof Event || reasonText === "[object Event]" || reasonText === "Event") {
if (e && typeof e.preventDefault === "function") e.preventDefault();
return;
}
if (reasonText && /UID2 shared library|UID2 API error/i.test(reasonText)) {
if (e && typeof e.preventDefault === "function") e.preventDefault();
return;
}
if (reasonText && !/ChessAssistant|userscript|tamper/i.test(reasonText)) {
return;
}
err("UnhandledRejection", reason);
});
// =====================================================
// Section 05: Local Error Handler
// =====================================================
(function attachLocalErrorHandler() {
window.addEventListener("error", function (e) {
if (!e || !e.filename) return;
if (!e.filename.includes("user") && !e.filename.includes("tamper")) return;
err(e.error || e.message);
});
})();
// =====================================================
// Section 06: Stealth Configuration
// =====================================================
let CONFIG = {
MAX_HISTORY_SIZE: 50,
MAX_ACPL_DISPLAY: 50,
MATE_VALUE: 50000,
MAX_BAR_CAP: 2000,
DEFAULT_DEPTH: 15,
MAX_DEPTH: 30,
PANEL_WIDTH: 340,
UPDATE_INTERVAL: 150,
FEN_POLL_INTERVAL: 300,
MAX_CACHE_SIZE: 500,
STEALTH: {
RANDOMIZE_DELAYS: true,
JITTER_RANGE: 0.15,
MOVE_TIME_VARIANCE: 0.25,
HUMAN_PAUSE_PROBABILITY: 0.1,
MAX_CONSISTENT_MOVES: 8,
BLUNDER_INJECTION_RATE: 0.05,
THINK_TIME_MIN: 800,
THINK_TIME_MAX: 3500,
},
EVASION: {
CLEAR_CONSOLE_LOGS: true,
MASK_GLOBAL_VARIABLES: true,
RANDOMIZE_CLASS_NAMES: false,
DISABLE_RIGHT_CLICK: false,
PREVENT_DEVTOOLS_DETECTION: true,
},
PREMOVE: {
MAX_EXECUTED_FENS: 100,
ENGINE_TIMEOUT: 8000,
EXECUTION_TIMEOUT: 5000,
RETRY_DELAY: 100,
MAX_RETRIES: 2
},
HUMAN: {
CRITICAL_CP_THRESHOLD: -120,
CRITICAL_MATE_PLY: 8,
CRITICAL_KING_ATTACKERS: 1,
DEBUG_DECISION: false,
LEVEL_TUNING: {
beginner: { errorMult: 1.20, blunderMult: 1.35, criticalErrorMult: 0.75, criticalBlunderMult: 0.45, safetyRiskCap: 70 },
casual: { errorMult: 1.10, blunderMult: 1.20, criticalErrorMult: 0.70, criticalBlunderMult: 0.35, safetyRiskCap: 65 },
intermediate: { errorMult: 1.00, blunderMult: 1.00, criticalErrorMult: 0.55, criticalBlunderMult: 0.20, safetyRiskCap: 60 },
advanced: { errorMult: 0.80, blunderMult: 0.70, criticalErrorMult: 0.45, criticalBlunderMult: 0.12, safetyRiskCap: 55 },
expert: { errorMult: 0.65, blunderMult: 0.55, criticalErrorMult: 0.35, criticalBlunderMult: 0.08, safetyRiskCap: 50 }
}
}
};
let ELO_LEVELS = {
beginner: { elo: 800, moveTime: { min: 1, max: 5 }, errorRate: 0.30, blunderRate: 0.15 },
casual: { elo: 1200, moveTime: { min: 2, max: 8 }, errorRate: 0.20, blunderRate: 0.10 },
intermediate: { elo: 1600, moveTime: { min: 3, max: 12 }, errorRate: 0.15, blunderRate: 0.05 },
advanced: { elo: 2000, moveTime: { min: 5, max: 15 }, errorRate: 0.10, blunderRate: 0.03 },
expert: { elo: 2400, moveTime: { min: 8, max: 20 }, errorRate: 0.05, blunderRate: 0.01 }
};
let PIECE_CHAR = {
"br": "r",
"bn": "n",
"bb": "b",
"bq": "q",
"bk": "k",
"bp": "p",
"wr": "R",
"wn": "N",
"wb": "B",
"wq": "Q",
"wk": "K",
"wp": "P"
};
let PIECE_VALUES = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 100 };
let _OPENING_BOOK_CACHE = null;
let _OPENING_NAMES_CACHE = null;
let _openingBookVersion = 0;
let _openingBookLoadState = { inFlight: false, lastAttemptTs: 0 };
function _loadOpeningBookExternal() {
if (_OPENING_BOOK_CACHE) return;
if (_openingBookLoadState.inFlight) return;
if (Date.now() - _openingBookLoadState.lastAttemptTs < 60000) return;
_openingBookLoadState.inFlight = true;
_openingBookLoadState.lastAttemptTs = Date.now();
function _extractLegacyObject(text, varName) {
let marker = "window." + varName + " =";
let idx = text.indexOf(marker);
if (idx === -1) return null;
let start = text.indexOf("{", idx);
if (start === -1) return null;
let depth = 0, inString = false, escaped = false;
for (let i = start; i < text.length; i++) {
let ch = text[i];
if (inString) {
if (escaped) escaped = false;
else if (ch === "\\") escaped = true;
else if (ch === '"') inString = false;
continue;
}
if (ch === '"') { inString = true; continue; }
if (ch === "{") depth++;
else if (ch === "}") {
depth--;
if (depth === 0) return text.slice(start, i + 1);
}
}
return null;
}
function _parseBookPayload(text) {
try {
let data = JSON.parse(text);
if (data && data.book && typeof data.book === "object") {
_OPENING_BOOK_CACHE = data.book;
_OPENING_NAMES_CACHE = data.names || null;
_openingBookVersion++;
if (typeof OpeningBook !== "undefined" && OpeningBook) OpeningBook._noEpIndex = null;
log("[OpeningBook] Loaded JSON " + Object.keys(_OPENING_BOOK_CACHE).length + " positions");
return true;
}
} catch (e) { }
try {
let bookText = _extractLegacyObject(text, "CHESS_OPENING_BOOK");
let namesText = _extractLegacyObject(text, "CHESS_OPENING_NAMES");
if (bookText) {
_OPENING_BOOK_CACHE = JSON.parse(bookText);
_OPENING_NAMES_CACHE = namesText ? JSON.parse(namesText) : null;
_openingBookVersion++;
if (typeof OpeningBook !== "undefined" && OpeningBook) OpeningBook._noEpIndex = null;
log("[OpeningBook] Loaded legacy JS " + Object.keys(_OPENING_BOOK_CACHE).length + " positions");
return true;
}
} catch (e2) {
warn("[OpeningBook] Parse failed:", e2);
}
return false;
}
try {
let raw = GM_getResourceText("openingbook");
if (raw && raw.length > 200 && _parseBookPayload(raw)) {
_openingBookLoadState.inFlight = false;
return;
}
} catch (e) { }
try {
GM_xmlhttpRequest({
method: "GET",
url: "https://raw.githubusercontent.com/JD-YH03D/release/refs/heads/main/Chess.com%20-%20Play%20Chess%20Online%20-%20Free%20Games/OpeningBook.json",
timeout: 8000,
onload: function (r) {
_openingBookLoadState.inFlight = false;
if (r && r.status === 200 && r.responseText && r.responseText.length > 200) {
_parseBookPayload(r.responseText);
}
},
onerror: function () {
_openingBookLoadState.inFlight = false;
warn("[OpeningBook] Fetch failed");
}
});
} catch (e2) {
_openingBookLoadState.inFlight = false;
warn("[OpeningBook] Request failed");
}
}
let OPENING_BOOK = new Proxy({}, {
get: function (target, prop) {
_loadOpeningBookExternal();
if (_OPENING_BOOK_CACHE && _OPENING_BOOK_CACHE[prop] !== undefined) return _OPENING_BOOK_CACHE[prop];
return _OPENING_BOOK_FALLBACK[prop];
},
ownKeys: function () {
_loadOpeningBookExternal();
let keys = _OPENING_BOOK_CACHE ? Object.keys(_OPENING_BOOK_CACHE) : [];
if (keys.length === 0) keys = Object.keys(_OPENING_BOOK_FALLBACK);
return keys;
},
getOwnPropertyDescriptor: function (target, prop) { return { configurable: true, enumerable: true, value: OPENING_BOOK[prop] }; }
});
let OPENING_NAMES = new Proxy({}, {
get: function (target, prop) {
_loadOpeningBookExternal();
if (_OPENING_NAMES_CACHE && _OPENING_NAMES_CACHE[prop] !== undefined) return _OPENING_NAMES_CACHE[prop];
return _OPENING_NAMES_FALLBACK[prop];
}
});
// Embedded fallback — 5 core positions for instant first-move response
let _OPENING_BOOK_FALLBACK = {
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -": {
"e2e4": 4, "d2d4": 3, "c2c4": 2, "g1f3": 2, "b1c3": 1
},
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3": {
"c7c5": 3, "e7e5": 3, "e7e6": 2, "c7c6": 2, "d7d5": 1
},
"rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6": {
"g1f3": 3, "f1c4": 2, "b1c3": 2, "d2d4": 1
},
"rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq d3": {
"d7d5": 3, "g8f6": 3, "c7c5": 2, "e7e6": 2
},
"rnbqkbnr/pppppppp/8/8/2P5/8/PP1PPPPP/RNBQKBNR b KQkq c3": {
"e7e5": 3, "c7c5": 2, "g8f6": 2, "e7e6": 2
}
};
let _OPENING_NAMES_FALLBACK = {
"e2e4": "King's Pawn Opening", "d2d4": "Queen's Pawn Game",
"c2c4": "English Opening", "g1f3": "Réti Opening",
"e7e5": "Open Game", "c7c5": "Sicilian Defense",
"e7e6": "French Defense", "c7c6": "Caro-Kann Defense",
"d7d5": "Scandinavian Defense", "g8f6": "Alekhine's Defense",
"f1c4": "Italian Game", "b1c3": "Vienna Game"
};
// =====================================================
// Section 07: Application State Variables
// =====================================================
let State = {
autoMovePiece: GM_getValue("autoMovePiece", false),
moveExecutionMode: GM_getValue("moveExecutionMode", "click"),
autoRun: GM_getValue("autoRun", false),
autoMatch: GM_getValue("autoMatch", false),
minDelay: GM_getValue("minDelay", 0.5),
maxDelay: GM_getValue("maxDelay", 3.0),
eloRating: GM_getValue("eloRating", 1600),
customDepth: GM_getValue("customDepth", CONFIG.DEFAULT_DEPTH),
skillLevel: GM_getValue("skillLevel", 20),
evaluationMode: GM_getValue("evaluationMode", "engine"),
panelTop: GM_getValue("panelTop", null),
panelLeft: GM_getValue("panelLeft", null),
panelState: GM_getValue("panelState", "maximized"),
onboardingAccepted: GM_getValue("onboardingAccepted", false),
highlightColor1: GM_getValue("highlightColor1", "#eb6150"),
highlightColor2: GM_getValue("highlightColor2", "#4287f5"),
analysisMode: GM_getValue("analysisMode", false),
highlightEnabled: GM_getValue("highlightEnabled", false),
autoAnalysisColor: GM_getValue("autoAnalysisColor", "none"),
useMainConsensus: GM_getValue("useMainConsensus", true),
analysisBlunderGuard: GM_getValue("analysisBlunderGuard", true),
analysisMinStableUpdates: GM_getValue("analysisMinStableUpdates", 2),
numberOfMovesToShow: GM_getValue("numberOfMovesToShow", 5),
clockSyncQuickDelayMs: GM_getValue("clockSyncQuickDelayMs", 300),
premoveEnabled: GM_getValue("premoveEnabled", false),
premoveMode: GM_getValue("premoveMode", "capture"),
premovePieces: GM_getValue("premovePieces", { q: 1, r: 1, b: 1, n: 1, p: 1 }),
premoveDepth: GM_getValue("premoveDepth", 5),
premoveRiskPenaltyFactor: GM_getValue("premoveRiskPenaltyFactor", 0.5),
premoveMinConfidence: GM_getValue("premoveMinConfidence", 5),
premoveDelayMs: GM_getValue("premoveDelayMs", 300),
premoveExecutedForFen: null,
premoveAnalysisInProgress: false,
premoveLastAnalysisTime: 0,
premoveThrottleMs: 500,
premoveRetryCount: 0,
lastNewGameLogTs: 0,
moveNumber: 1,
incrementSeconds: 0,
humanLevel: GM_getValue("humanLevel", "intermediate"),
useOpeningBook: GM_getValue("useOpeningBook", true),
showPVArrows: GM_getValue("showPVArrows", false),
pvArrowColors: GM_getValue("pvArrowColors", {
1: "#4287f5",
2: "#eb6150",
3: "#4caf50",
4: "#9c27b0",
5: "#f38ba8",
6: "#fab387",
7: "#74c7ec",
8: "#f5c2e7",
9: "#b4befe"
}),
showBestmoveArrows: GM_getValue("showBestmoveArrows", false),
bestmoveArrowColor: GM_getValue("bestmoveArrowColor", "#f9e2af"),
bestmoveArrowColors: GM_getValue("bestmoveArrowColors", {
1: "#eb6150",
2: "#89b4fa",
3: "#a6e3a1",
4: "#f38ba8",
5: "#cba6f7",
6: "#fab387",
7: "#74c7ec",
8: "#f5c2e7",
9: "#b4befe"
}),
maxPVDepth: GM_getValue("maxPVDepth", 2),
autoDepthAdapt: GM_getValue("autoDepthAdapt", false),
mainPVLine: [],
mainPVTurn: "w",
lastRenderedMainPV: "",
lastMainPVDrawTime: 0,
_mainDepthByPv: {},
analysisPVLine: [],
analysisPVTurn: "w",
lastRenderedAnalysisPV: "",
lastAnalysisPVDrawTime: 0,
_analysisDepthByPv: {},
_lastAnalysisFen: null,
_lastAnalysisDepth: 0,
_lastAnalysisBestPV: [],
_lastAnalysisBestMove: null,
_prePremoveState: null,
_preAnalysisState: null,
_preSmartControlsState: null,
_smartControlsForcedByAutoPlay: false,
_lastScoreInfo: null,
_lastPremoveScoreInfo: null,
isThinking: false,
loopStarted: false,
gameEnded: false,
lastAutoRunFen: null,
currentEvaluation: 0,
evalBarSmoothedCp: 0,
evalBarInitialized: false,
lastEvalDeltaCp: 0,
_lastEvalRawCp: null,
previousEvaluation: 0,
currentPVTurn: "w",
analysisStableCount: 0,
analysisLastBestMove: "",
analysisLastEvalCp: null,
analysisPrevEvalCp: null,
analysisGuardStateText: "Ready",
mainBestHistory: [],
totalCplWhite: 0,
cplMoveCountWhite: 0,
acplWhite: "0.00",
totalCplBlack: 0,
cplMoveCountBlack: 0,
acplBlack: "0.00",
acplInitialized: false,
_analysisAutoPlayApproved: false,
_analysisAutoPlayMove: null,
topMoves: [],
topMoveInfos: {},
topMovesFen: "",
lastTopMove1: "...",
lastEvalText1: "0.00",
lastMoveGrade: "Book",
lastEvalClass1: "eval-equal",
principalVariation: "",
statusInfo: "",
evalBarStatus: "Loading...",
isAnalysisThinking: false,
currentDelayMs: 0,
moveExecutionInProgress: false,
lastError: "",
autoResignEnabled: GM_getValue("autoResignEnabled", false),
resignMode: GM_getValue("resignMode", "mate"),
autoResignThresholdMate: GM_getValue("autoResignThresholdMate", 3),
autoResignThresholdCp: GM_getValue("autoResignThresholdCp", 1000),
clockSync: GM_getValue("clockSync", false),
clockSyncMinDelay: GM_getValue("clockSyncMinDelay", 1.5),
clockSyncMaxDelay: GM_getValue("clockSyncMaxDelay", 5.0),
clockSyncLowTimeQuickSec: GM_getValue("clockSyncLowTimeQuickSec", 20),
cctAnalysisEnabled: GM_getValue("cctAnalysisEnabled", true),
cctComponents: GM_getValue("cctComponents", { checks: true, captures: true, threats: true }),
cctDebugEnabled: GM_getValue("cctDebugEnabled", false),
silentLogging: GM_getValue("silentLogging", false),
moveStartTime: 0,
notationSequence: GM_getValue("notationSequence", ""),
premoveStats: {
attempted: 0,
allowed: 0,
executed: 0,
blocked: 0,
failed: 0
},
premoveLiveChance: 0,
premoveTargetChance: 0,
premoveLastEvalDisplay: "-",
premoveLastMoveDisplay: "-",
premoveChanceReason: "Waiting for position...",
premoveChanceUpdatedTs: 0,
cctLastDebugText: "CCT debug idle",
syzygyStatus: "Idle",
syzygyLastFen: "",
syzygySource: "",
syzygyMoves: [],
syzygyMeta: null,
syzygyError: "",
analysisHistoryCursor: 0,
analysisAcplFen: "",
analysisEvalText: "0.00",
analysisLastRecordedKey: ""
};
const PERSISTED_SETTING_DEFAULTS = {
autoMovePiece: false,
moveExecutionMode: "click",
autoRun: false,
autoMatch: false,
minDelay: 0.5,
maxDelay: 3.0,
eloRating: 1600,
customDepth: CONFIG.DEFAULT_DEPTH,
skillLevel: 20,
evaluationMode: "engine",
panelTop: null,
panelLeft: null,
panelState: "maximized",
onboardingAccepted: false,
highlightColor1: "#eb6150",
highlightColor2: "#4287f5",
analysisMode: false,
highlightEnabled: false,
autoAnalysisColor: "none",
useMainConsensus: true,
analysisBlunderGuard: true,
analysisMinStableUpdates: 2,
analysisGuardStateText: "Ready",
numberOfMovesToShow: 5,
clockSyncQuickDelayMs: 300,
premoveEnabled: false,
premoveMode: "capture",
premovePieces: { q: 1, r: 1, b: 1, n: 1, p: 1 },
premoveDepth: 5,
premoveRiskPenaltyFactor: 0.5,
premoveMinConfidence: 5,
premoveDelayMs: 300,
humanLevel: "intermediate",
useOpeningBook: true,
showPVArrows: false,
pvArrowColors: {
1: "#4287f5",
2: "#eb6150",
3: "#4caf50",
4: "#9c27b0",
5: "#f38ba8",
6: "#fab387",
7: "#74c7ec",
8: "#f5c2e7",
9: "#b4befe"
},
showBestmoveArrows: false,
bestmoveArrowColor: "#f9e2af",
bestmoveArrowColors: {
1: "#eb6150",
2: "#89b4fa",
3: "#a6e3a1",
4: "#f38ba8",
5: "#cba6f7",
6: "#fab387",
7: "#74c7ec",
8: "#f5c2e7",
9: "#b4befe"
},
maxPVDepth: 2,
autoDepthAdapt: false,
autoResignEnabled: false,
resignMode: "mate",
autoResignThresholdMate: 3,
autoResignThresholdCp: 1000,
clockSync: false,
clockSyncMinDelay: 1.5,
clockSyncMaxDelay: 5.0,
clockSyncLowTimeQuickSec: 20,
cctAnalysisEnabled: true,
cctComponents: { checks: true, captures: true, threats: true },
cctDebugEnabled: false,
silentLogging: false,
notationSequence: ""
};
const SETTING_NUMBER_LIMITS = {
minDelay: [0.05, 60],
maxDelay: [0.05, 60],
eloRating: [300, 3200],
customDepth: [1, CONFIG.MAX_DEPTH],
skillLevel: [0, 20],
analysisMinStableUpdates: [1, 5],
numberOfMovesToShow: [2, 10],
clockSyncQuickDelayMs: [100, 5000],
premoveDepth: [1, CONFIG.MAX_DEPTH],
premoveRiskPenaltyFactor: [0, 2],
premoveMinConfidence: [1, 95],
premoveDelayMs: [50, 2000],
maxPVDepth: [2, 10],
autoResignThresholdMate: [1, 10],
autoResignThresholdCp: [100, 5000],
clockSyncMinDelay: [0.1, 30],
clockSyncMaxDelay: [0.1, 60],
clockSyncLowTimeQuickSec: [1, 300],
panelTop: [0, 10000],
panelLeft: [0, 10000]
};
function sanitizeSettingValue(key, rawValue) {
const defaultValue = PERSISTED_SETTING_DEFAULTS[key];
if (defaultValue === undefined) return rawValue;
if (key === "panelTop" || key === "panelLeft") {
if (rawValue === null || rawValue === undefined || rawValue === "") return null;
}
if (key === "premovePieces") {
const base = { q: 1, r: 1, b: 1, n: 1, p: 1 };
const src = (rawValue && typeof rawValue === "object") ? rawValue : base;
return {
q: src.q ? 1 : 0,
r: src.r ? 1 : 0,
b: src.b ? 1 : 0,
n: src.n ? 1 : 0,
p: src.p ? 1 : 0
};
}
if (key === "cctComponents") {
const base = { checks: true, captures: true, threats: true };
const src = (rawValue && typeof rawValue === "object") ? rawValue : base;
return {
checks: !!src.checks,
captures: !!src.captures,
threats: !!src.threats
};
}
if (key === "pvArrowColors") {
const base = {
1: "#4287f5",
2: "#eb6150",
3: "#4caf50",
4: "#9c27b0",
5: "#f38ba8",
6: "#fab387",
7: "#74c7ec",
8: "#f5c2e7",
9: "#b4befe"
};
const src = (rawValue && typeof rawValue === "object") ? rawValue : base;
const out = {};
for (let i = 1; i <= 9; i++) {
const raw = src[i] || src[String(i)] || base[i];
out[i] = /^#[0-9a-fA-F]{6}$/.test(String(raw)) ? String(raw) : base[i];
}
return out;
}
if (key === "bestmoveArrowColors") {
const base = {
1: "#eb6150",
2: "#89b4fa",
3: "#a6e3a1",
4: "#f38ba8",
5: "#cba6f7",
6: "#fab387",
7: "#74c7ec",
8: "#f5c2e7",
9: "#b4befe"
};
const src = (rawValue && typeof rawValue === "object") ? rawValue : base;
const out = {};
for (let i = 1; i <= 9; i++) {
const raw = src[i] || src[String(i)] || base[i];
out[i] = /^#[0-9a-fA-F]{6}$/.test(String(raw)) ? String(raw) : base[i];
}
return out;
}
if (key === "moveExecutionMode") {
return rawValue === "drag" ? "drag" : "click";
}
if (key === "evaluationMode") {
return rawValue === "human" ? "human" : "engine";
}
if (key === "panelState") {
return ["maximized", "minimized", "closed"].includes(rawValue) ? rawValue : "maximized";
}
if (key === "autoAnalysisColor") {
return ["white", "black", "none"].includes(rawValue) ? rawValue : "none";
}
if (key === "premoveMode") {
return ["every", "capture", "filter"].includes(rawValue) ? rawValue : "capture";
}
if (key === "humanLevel") {
return ELO_LEVELS[rawValue] ? rawValue : "intermediate";
}
if (key === "resignMode") {
return rawValue === "cp" ? "cp" : "mate";
}
if (key === "highlightColor1" || key === "highlightColor2" || key === "bestmoveArrowColor") {
return /^#[0-9a-fA-F]{6}$/.test(String(rawValue)) ? String(rawValue) : defaultValue;
}
if (typeof defaultValue === "boolean") {
return !!rawValue;
}
if (typeof defaultValue === "number") {
const n = Number(rawValue);
if (!Number.isFinite(n)) return defaultValue;
const limits = SETTING_NUMBER_LIMITS[key];
if (!limits) return n;
return clamp(n, limits[0], limits[1]);
}
if (typeof defaultValue === "string") {
return String(rawValue || "");
}
return rawValue;
}
function normalizeLoadedSettings() {
Object.keys(PERSISTED_SETTING_DEFAULTS).forEach(function (key) {
const sanitized = sanitizeSettingValue(key, State[key]);
if (JSON.stringify(sanitized) !== JSON.stringify(State[key])) {
State[key] = sanitized;
GM_setValue(key, sanitized);
}
});
if (State.clockSyncMinDelay > State.clockSyncMaxDelay) {
const temp = State.clockSyncMinDelay;
State.clockSyncMinDelay = State.clockSyncMaxDelay;
State.clockSyncMaxDelay = temp;
GM_setValue("clockSyncMinDelay", State.clockSyncMinDelay);
GM_setValue("clockSyncMaxDelay", State.clockSyncMaxDelay);
}
if (State.minDelay > State.maxDelay) {
const temp = State.minDelay;
State.minDelay = State.maxDelay;
State.maxDelay = temp;
GM_setValue("minDelay", State.minDelay);
GM_setValue("maxDelay", State.maxDelay);
}
}
normalizeLoadedSettings();
// =====================================================
// Section 08: Cache Variables
// =====================================================
let cachedGame = null;
let cachedGameTimestamp = 0;
let GAME_CACHE_TTL = 100;
let pendingMoveTimeoutId = null;
let _resignObserver = null;
let _resignTimeout = null;
let _resignTriggerCount = 0;
let _resignTriggerNeeded = 3;
let _suppressNewGameActionUntil = 0;
// stockfishSourceCode now accessed via EngineLoader.stockfishSourceCode
let _allLoopsActive = true;
let _premoveCacheClearInterval = null;
let _panelHotkeysBound = false;
let _gearMenuDocBound = false;
let _eventListeners = [];
let _loopTimeoutIds = new Set();
function scheduleManagedTimeout(fn, delay) {
let id = null;
id = setTimeout(function () {
_loopTimeoutIds.delete(id);
fn();
}, delay);
_loopTimeoutIds.add(id);
return id;
}
function clearManagedTimeouts() {
_loopTimeoutIds.forEach(function (id) {
clearTimeout(id);
});
_loopTimeoutIds.clear();
}
function trimTimeoutIds() {
if (_loopTimeoutIds.size > 100) {
log("[Runtime] Trimming timeout IDs: " + _loopTimeoutIds.size + " → 0");
clearManagedTimeouts();
}
}
function cleanupEventListeners() {
if (_eventListeners && _eventListeners.length > 0) {
_eventListeners.forEach(function (listener) {
try {
listener.element.removeEventListener(listener.type, listener.handler, listener.options || false);
} catch (e) {
}
});
_eventListeners = [];
}
}
let RuntimeGuard = {
loopPerf: Object.create(null),
lastPerfLogTs: 0,
lastCacheAlertTs: 0,
lastSoakLogTs: 0,
lastUIHealTs: 0,
uiHealCount: 0,
listenerHealCount: 0,
selfTestRuns: 0,
selfTestFailures: 0,
premoveStuckSince: 0,
mainStuckSince: 0,
analysisStuckSince: 0,
premoveHealCount: 0,
mainHealCount: 0,
analysisHealCount: 0,
_nowMs: function () {
if (typeof performance !== "undefined" && typeof performance.now === "function") {
return performance.now();
}
return Date.now();
},
trackLoop: function (name, startTs) {
const elapsed = this._nowMs() - startTs;
let stat = this.loopPerf[name];
if (!stat) {
stat = { count: 0, total: 0, max: 0 };
this.loopPerf[name] = stat;
}
stat.count++;
stat.total += elapsed;
if (elapsed > stat.max) stat.max = elapsed;
if (elapsed > 120) {
warn("[Perf] Slow loop:", name, Math.round(elapsed) + "ms");
}
const now = Date.now();
if (stat.count % 120 === 0 && now - this.lastPerfLogTs > 30000) {
const avg = stat.total / Math.max(1, stat.count);
log("[Perf]", name, "avg=" + avg.toFixed(1) + "ms", "max=" + stat.max.toFixed(1) + "ms", "runs=" + stat.count);
this.lastPerfLogTs = now;
}
},
checkCachePressure: function () {
const issues = [];
if (Engine && Engine._premoveProcessedFens && Engine._premoveProcessedFens.size > 25) {
issues.push("premoveProcessed=" + Engine._premoveProcessedFens.size);
const keep = 12;
const arr = Array.from(Engine._premoveProcessedFens);
Engine._premoveProcessedFens = new Set(arr.slice(-keep));
}
if (CCTAnalyzer && CCTAnalyzer.cache && CCTAnalyzer.cache.size > 260) {
issues.push("cctCache=" + CCTAnalyzer.cache.size);
CCTAnalyzer.clearCache();
}
if (ThreatDetectionSystem && ThreatDetectionSystem.cache && ThreatDetectionSystem.cache.size > 260) {
issues.push("threatCache=" + ThreatDetectionSystem.cache.size);
ThreatDetectionSystem.clearCache();
}
if (Syzygy && Syzygy.cache && Syzygy.cache.size > 80) {
issues.push("syzygyCache=" + Syzygy.cache.size);
Syzygy.clear();
}
if (issues.length > 0) {
const now = Date.now();
if (now - this.lastCacheAlertTs > 10000) {
warn("[Watchdog] Cache pressure:", issues.join(", "));
this.lastCacheAlertTs = now;
}
}
},
checkPremoveWatchdog: function () {
if (!State.premoveEnabled || State.analysisMode) {
this.premoveStuckSince = 0;
return;
}
const active = !!(Engine._premoveEngineBusy || Engine._premoveProcessing || State.premoveAnalysisInProgress);
if (!active) {
this.premoveStuckSince = 0;
return;
}
const now = Date.now();
const lastActivity = Engine._premoveLastActivityTs || 0;
if (!lastActivity) {
Engine._premoveLastActivityTs = now;
return;
}
const timeoutMs = (CONFIG.PREMOVE.ENGINE_TIMEOUT || 8000) + 3000;
if (now - lastActivity < timeoutMs) {
this.premoveStuckSince = 0;
return;
}
if (!this.premoveStuckSince) {
this.premoveStuckSince = now;
return;
}
if (now - this.premoveStuckSince < 1500) {
return;
}
this.premoveHealCount++;
warn("[Watchdog] Premove stuck detected. Healing worker...");
if (Engine && typeof Engine.selfHealPremove === "function") {
Engine.selfHealPremove("watchdog-timeout");
}
this.premoveStuckSince = 0;
},
checkEngineWatchdog: function () {
const now = Date.now();
const mainActive = !!(Engine && Engine.main && Engine._ready && State.isThinking && !State.analysisMode);
if (!mainActive) {
this.mainStuckSince = 0;
} else {
const mainLast = Engine._mainLastActivityTs || 0;
if (mainLast && now - mainLast > 12000) {
if (!this.mainStuckSince) {
this.mainStuckSince = now;
} else if (now - this.mainStuckSince > 1500) {
this.mainHealCount++;
warn("[Watchdog] Main engine stuck detected. Healing worker...");
if (typeof Engine.selfHealMain === "function") {
Engine.selfHealMain("watchdog-timeout");
}
this.mainStuckSince = 0;
}
} else {
this.mainStuckSince = 0;
}
}
const analysisActive = !!(Engine && Engine.analysis && State.analysisMode && State.isAnalysisThinking);
if (!analysisActive) {
this.analysisStuckSince = 0;
} else {
const analysisLast = Engine._analysisLastActivityTs || 0;
if (analysisLast && now - analysisLast > 12000) {
if (!this.analysisStuckSince) {
this.analysisStuckSince = now;
} else if (now - this.analysisStuckSince > 1500) {
this.analysisHealCount++;
warn("[Watchdog] Analysis engine stuck detected. Healing worker...");
if (typeof Engine.selfHealAnalysis === "function") {
Engine.selfHealAnalysis("watchdog-timeout");
}
this.analysisStuckSince = 0;
}
} else {
this.analysisStuckSince = 0;
}
}
},
checkUIWatchdog: function () {
const now = Date.now();
const panel = $("#chess-assist-panel");
if (!panel) return;
let lastUiTs = UI && typeof UI._lastHeartbeatTs === "number" ? UI._lastHeartbeatTs : 0;
if (!lastUiTs || now - lastUiTs < 15000) return;
if (now - this.lastUIHealTs < 10000) return;
this.lastUIHealTs = now;
this.uiHealCount++;
warn("[Watchdog] UI heartbeat stale, healing UI render");
try {
renderAll();
} catch (e) {
err("[Watchdog] renderAll heal failed:", e);
}
try {
setupAllListeners();
this.listenerHealCount++;
} catch (e) {
err("[Watchdog] setupAllListeners heal failed:", e);
}
},
logSoakSummary: function () {
const now = Date.now();
if (now - this.lastSoakLogTs < 60000) return;
this.lastSoakLogTs = now;
const loops = Object.keys(this.loopPerf).map((k) => {
const p = this.loopPerf[k];
const avg = p && p.count ? (p.total / p.count).toFixed(1) : "0.0";
return k + ":" + avg + "ms";
}).join(" | ");
log(
"[Soak]",
"caches CCT=" + (CCTAnalyzer && CCTAnalyzer.cache ? CCTAnalyzer.cache.size : 0) +
" TH=" + (ThreatDetectionSystem && ThreatDetectionSystem.cache ? ThreatDetectionSystem.cache.size : 0),
"heals P=" + this.premoveHealCount +
" M=" + this.mainHealCount +
" A=" + this.analysisHealCount,
loops
);
},
getSnapshot: function () {
return {
loops: this.loopPerf,
premoveHealCount: this.premoveHealCount,
mainHealCount: this.mainHealCount,
analysisHealCount: this.analysisHealCount,
uiHealCount: this.uiHealCount,
listenerHealCount: this.listenerHealCount,
selfTestRuns: this.selfTestRuns,
selfTestFailures: this.selfTestFailures
};
}
};
// =====================================================
// Section 09: Ultra-Smart Premove Engine
// =====================================================
const SmartPremove = {
lastSafeMoves: [],
moveHistory: [],
executedFens: new Set(),
executionLock: false,
processingLock: false,
lastExecutionTime: 0,
consecutiveErrors: 0,
patternBreakCounter: 0,
lastBlunderFen: null,
blunderCount: 0,
MAX_EXECUTED_FENS: 15,
MAX_HISTORY: 20,
MAX_CONSECUTIVE_ERRORS: 3,
MIN_EXECUTION_INTERVAL: 200,
ERROR_COOLDOWN: 5000,
BLUNDER_COOLDOWN: 8000,
PIECE_ORDER: ['p', 'n', 'b', 'r', 'q'],
RISK_MULTIPLIERS: {
SAFE_THRESHOLD: 10,
BLOCK_THRESHOLD: 15,
PIECE_HANGING: 12,
BAD_TRADE: 7,
RISK_LEVEL_DIVISOR: 5,
KING_SAFETY_WEIGHT: 25,
PIN_PENALTY: 15,
DISCOVERED_CHECK_PENALTY: 30
},
PATTERN_DETECTION: {
MIN_MOVES: 5,
VARIANCE_THRESHOLD: 0.1,
ACCURACY_THRESHOLD: 0.95,
VARIANCE_WEIGHT: 40,
ACCURACY_WEIGHT: 60
},
AGGRESSION_CONFIG: {
every: {
minConfidence: 20,
riskTolerance: 35,
tacticalBonus: 15,
allowSpeculative: true,
maxRiskScore: 220,
requireSafetyCheck: true,
minEvalForSpeculative: -200,
rollBuffer: 12,
maxConfidenceBoost: 8,
},
capture: {
minConfidence: 40,
riskTolerance: 15,
tacticalBonus: 8,
allowSpeculative: false,
maxRiskScore: 100,
requireSafetyCheck: true,
requirePositiveExchange: true,
minSEEValue: 0,
minCaptureValue: 0,
avoidLossyCapture: true,
maxNetLoss: 0,
rollBuffer: -5,
},
filter: {
minConfidence: 32,
riskTolerance: 22,
tacticalBonus: 10,
allowSpeculative: false,
maxRiskScore: 180,
requireSafetyCheck: true,
requireMoveQuality: true,
minTacticalScore: -3,
rollBuffer: 0,
}
},
resetExecutionTracking() {
this.executedFens.clear();
this.executionLock = false;
this.processingLock = false;
this.consecutiveErrors = 0;
this.patternBreakCounter = 0;
this.moveHistory = [];
this.lastSafeMoves = [];
this.lastExecutionTime = 0;
this.lastBlunderFen = null;
this.blunderCount = 0;
this._invalidateAllTokens();
},
isInErrorCooldown() {
if (this.consecutiveErrors >= this.MAX_CONSECUTIVE_ERRORS) {
const timeSinceLastError = Date.now() - this.lastExecutionTime;
if (timeSinceLastError < this.ERROR_COOLDOWN) {
return true;
}
this.consecutiveErrors = 0;
}
return false;
},
recordBlunder(fen) {
this.blunderCount++;
this.lastBlunderFen = fen;
if (this.blunderCount >= 2) {
log("[SmartPremove] Multiple blunders - entering cautious mode");
}
},
isInCautiousMode() {
if (this.blunderCount >= 3) return true;
if (this.blunderCount >= 2 &&
Date.now() - this.lastExecutionTime < this.BLUNDER_COOLDOWN) {
return true;
}
return false;
},
detectPattern() {
if (this.moveHistory.length < this.PATTERN_DETECTION.MIN_MOVES) {
return null;
}
const last = this.moveHistory.slice(-this.PATTERN_DETECTION.MIN_MOVES);
const variance = this._timeVariance(last);
const accuracy = this._engineAccuracy(last);
const isTooConsistent =
variance < this.PATTERN_DETECTION.VARIANCE_THRESHOLD &&
accuracy > this.PATTERN_DETECTION.ACCURACY_THRESHOLD;
const varianceRisk = Math.max(0, 1 - variance / this.PATTERN_DETECTION.VARIANCE_THRESHOLD);
const accuracyRisk = accuracy;
const riskLevel = Math.min(
100,
varianceRisk * this.PATTERN_DETECTION.VARIANCE_WEIGHT +
accuracyRisk * this.PATTERN_DETECTION.ACCURACY_WEIGHT
);
return {
isTooConsistent,
riskLevel,
variance,
accuracy,
moveCount: last.length
};
},
_timeVariance(moves) {
if (!moves || moves.length < 2) return 0;
const times = moves.map(m => m.timeSpent || 0);
const mean = times.reduce((a, b) => a + b, 0) / times.length;
if (mean === 0) return 0;
const variance = times.reduce((s, t) => s + (t - mean) ** 2, 0) / times.length;
return Math.sqrt(variance) / mean;
},
_engineAccuracy(moves) {
if (!moves || moves.length === 0) return 0;
return moves.filter(m => m.wasEngineMove).length / moves.length;
},
analyzeTacticalMotifs(fen, uci, ourColor) {
const empty = { score: 0, isBlunder: false, isBrilliant: false, details: [] };
if (!fen || !uci || uci.length < 4) return empty;
if (!ourColor || (ourColor !== "w" && ourColor !== "b")) return empty;
const from = uci.slice(0, 2);
const to = uci.slice(2, 4);
const promo = uci.length > 4 ? uci[4] : null;
const movingPiece = pieceFromFenChar(fenCharAtSquare(fen, from));
if (!movingPiece || movingPiece.color !== ourColor) return empty;
const newFen = makeSimpleMove(fen, from, to, promo);
if (!newFen) return empty;
const motifs = { score: 0, isBlunder: false, isBrilliant: false, details: [] };
const oppColor = ourColor === "w" ? "b" : "w";
const oppKing = findKing(newFen, oppColor);
if (oppKing && isSquareAttackedBy(newFen, oppKing, ourColor)) {
if (isCheckmate(newFen, oppColor)) {
motifs.score += 1000;
motifs.isBrilliant = true;
motifs.details.push("Checkmate!");
return motifs;
}
motifs.score += 8;
motifs.details.push("Check");
const checkAttackers = getAttackersOfSquare(newFen, to, oppColor);
const checkDefenders = getAttackersOfSquare(newFen, to, ourColor);
if (checkAttackers.length > 0 && checkDefenders.length === 0) {
const pieceVal = PIECE_VALUES[movingPiece.type] || 0;
if (pieceVal > 3) {
motifs.score -= pieceVal * 2;
motifs.details.push(`Check BUT ${movingPiece.type.toUpperCase()} hangs (-${pieceVal * 2})`);
}
}
}
const capturedCh = fenCharAtSquare(fen, to);
const capturedPiece = pieceFromFenChar(capturedCh);
if (capturedPiece && capturedPiece.color === oppColor) {
const seeResult = this._staticExchangeEval(fen, from, to, ourColor);
if (seeResult >= 0) {
const capturedVal = PIECE_VALUES[capturedPiece.type] || 0;
motifs.score += capturedVal + seeResult;
motifs.details.push(`Good capture: ${capturedPiece.type.toUpperCase()} (SEE: +${seeResult})`);
} else {
motifs.score += seeResult;
motifs.details.push(`Bad capture: lose ${Math.abs(seeResult)} in exchange`);
if (seeResult < -2) {
motifs.isBlunder = true;
motifs.details.push("BLUNDER: losing exchange");
}
}
}
if (isEnPassantCapture(fen, from, to, ourColor)) {
motifs.score += 1;
motifs.details.push("En passant capture");
}
const ourHanging = this._findOurHangingPieces(newFen, ourColor);
if (ourHanging.length > 0) {
const hangValue = ourHanging.reduce((s, p) => s + (PIECE_VALUES[p.type] || 0), 0);
motifs.score -= hangValue * 2;
motifs.details.push(`Our pieces hanging: ${ourHanging.map(p => p.type.toUpperCase()).join(",")} (-${hangValue * 2})`);
if (hangValue >= 5) {
motifs.isBlunder = true;
}
}
const oppHanging = this._findHangingPieces(newFen, oppColor);
if (oppHanging.length > 0) {
const value = oppHanging.reduce((s, p) => s + (PIECE_VALUES[p.type] || 0), 0);
motifs.score += Math.min(value, 8);
motifs.details.push(`Opponent hanging pieces (${value})`);
}
const forkResult = this._detectForkAfterMove(newFen, to, movingPiece.type, ourColor);
if (forkResult) {
motifs.score += forkResult.value;
motifs.isBrilliant = motifs.isBrilliant || forkResult.value >= 8;
motifs.details.push(forkResult.description);
}
const pinResult = this._detectPinAfterMove(newFen, to, movingPiece.type, ourColor);
if (pinResult) {
motifs.score += 3;
motifs.details.push(pinResult.description);
}
const ourKing = findKing(newFen, ourColor);
if (ourKing && isSquareAttackedBy(newFen, ourKing, oppColor)) {
motifs.score -= 100;
motifs.isBlunder = true;
motifs.details.push("BLUNDER: Our king in check after move!");
}
const toAttackers = getAttackersOfSquare(newFen, to, oppColor);
const toDefenders = getAttackersOfSquare(newFen, to, ourColor);
if (toAttackers.length > 0 && toDefenders.length === 0 && !capturedPiece) {
const pieceVal = PIECE_VALUES[movingPiece.type] || 0;
if (pieceVal >= 3) {
motifs.score -= pieceVal * 2;
motifs.details.push(`Moving ${movingPiece.type.toUpperCase()} to undefended square (-${pieceVal * 2})`);
if (pieceVal >= 5) motifs.isBlunder = true;
}
}
if (promo) {
motifs.score += PIECE_VALUES[promo] || 9;
motifs.details.push(`Promotion to ${promo.toUpperCase()}`);
}
if (movingPiece.type === 'p') {
const promoRank = ourColor === 'w' ? 8 : 1;
const toRank = parseInt(to[1], 10);
const distance = Math.abs(toRank - promoRank);
if (distance === 1) {
motifs.score += 4;
motifs.details.push("Pawn 1 square from promotion");
} else if (distance === 2) {
motifs.score += 2;
motifs.details.push("Pawn 2 squares from promotion");
}
}
return motifs;
},
_staticExchangeEval(fen, from, to, ourColor) {
const oppColor = ourColor === "w" ? "b" : "w";
const movingPiece = pieceFromFenChar(fenCharAtSquare(fen, from));
const capturedPiece = pieceFromFenChar(fenCharAtSquare(fen, to));
if (!movingPiece || !capturedPiece) return 0;
const afterMoveFen = makeSimpleMove(fen, from, to);
if (!afterMoveFen) return 0;
const oppAttackers = getAttackersOfSquare(afterMoveFen, to, oppColor).map(a => PIECE_VALUES[a.piece] || 0).sort((a, b) => a - b);
const ourDefenders = getAttackersOfSquare(afterMoveFen, to, ourColor).map(a => PIECE_VALUES[a.piece] || 0).sort((a, b) => a - b);
const capturedVal = PIECE_VALUES[capturedPiece.type] || 0;
const ourPieceVal = PIECE_VALUES[movingPiece.type] || 0;
const gains = [capturedVal];
let currentPieceVal = ourPieceVal;
let oppIdx = 0, ourIdx = 0, isOppTurn = true;
for (let depth = 0; depth < 16; depth++) {
if (isOppTurn) { if (oppIdx >= oppAttackers.length) break; gains.push(currentPieceVal); currentPieceVal = oppAttackers[oppIdx++]; }
else { if (ourIdx >= ourDefenders.length) break; gains.push(currentPieceVal); currentPieceVal = ourDefenders[ourIdx++]; }
isOppTurn = !isOppTurn;
}
let value = 0;
for (let i = gains.length - 1; i > 0; i--) { value = Math.max(0, gains[i] - value); }
return gains[0] - value;
},
_findOurHangingPieces(fen, ourColor) {
if (!fen || !ourColor) return [];
const pieces = getAllPieces(fen, ourColor);
const oppColor = ourColor === "w" ? "b" : "w";
const result = [];
for (const p of pieces) {
if (p.type === "k") continue;
const attackers = getAttackersOfSquare(fen, p.square, oppColor);
if (!attackers.length) continue;
const defenders = getAttackersOfSquare(fen, p.square, ourColor);
const val = PIECE_VALUES[p.type] || 0;
if (!defenders.length) { result.push(p); continue; }
const minAtk = Math.min(...attackers.map(a => PIECE_VALUES[a.piece] || 99));
if (minAtk < val) result.push(p);
}
return result;
},
_findHangingPieces(fen, color) {
if (!fen || !color) return [];
const pieces = getAllPieces(fen, color);
const opp = color === "w" ? "b" : "w";
const result = [];
for (const p of pieces) {
if (p.type === "k") continue;
const attackers = getAttackersOfSquare(fen, p.square, opp);
if (!attackers.length) continue;
const defenders = getAttackersOfSquare(fen, p.square, color);
const val = PIECE_VALUES[p.type] || 0;
const minAtk = Math.min(...attackers.map(a => PIECE_VALUES[a.piece] || 99));
if (!defenders.length || minAtk < val) result.push(p);
}
return result;
},
_detectForkAfterMove(fen, square, pieceType, ourColor) {
const oppColor = ourColor === "w" ? "b" : "w";
const attacked = getSquaresAttackedByPiece(fen, square, pieceType, ourColor);
const valuableTargets = [];
for (const sq of attacked) {
const piece = pieceFromFenChar(fenCharAtSquare(fen, sq));
if (piece && piece.color === oppColor) {
const val = PIECE_VALUES[piece.type] || 0;
if (val >= 3 || piece.type === 'k') { valuableTargets.push({ square: sq, type: piece.type, value: val }); }
}
}
if (valuableTargets.length >= 2) {
const totalValue = valuableTargets.reduce((s, t) => s + t.value, 0);
const hasKing = valuableTargets.some(t => t.type === 'k');
return { value: hasKing ? Math.min(totalValue, 15) : Math.min(totalValue, 10), targets: valuableTargets, description: `Fork: ${pieceType.toUpperCase()} attacks ${valuableTargets.map(t => t.type.toUpperCase()).join(" & ")}` };
}
return null;
},
_detectPinAfterMove(fen, square, pieceType, ourColor) {
if (!['q', 'r', 'b'].includes(pieceType)) return null;
const oppColor = ourColor === "w" ? "b" : "w";
const oppKing = findKing(fen, oppColor);
if (!oppKing) return null;
const directions = [];
if (pieceType === 'q' || pieceType === 'r') directions.push([1, 0], [-1, 0], [0, 1], [0, -1]);
if (pieceType === 'q' || pieceType === 'b') directions.push([1, 1], [1, -1], [-1, 1], [-1, -1]);
const sf = "abcdefgh".indexOf(square[0]);
const sr = parseInt(square[1], 10);
for (const [dx, dy] of directions) {
let f = sf + dx, r = sr + dy, firstPiece = null;
while (f >= 0 && f <= 7 && r >= 1 && r <= 8) {
const sq = "abcdefgh"[f] + r;
const ch = fenCharAtSquare(fen, sq);
if (ch) {
const p = pieceFromFenChar(ch);
if (!p) break;
if (!firstPiece) { if (p.color === oppColor) { firstPiece = { square: sq, piece: p }; } else { break; } }
else { if (p.color === oppColor && p.type === 'k') { return { pinnedPiece: firstPiece.piece.type, pinnedSquare: firstPiece.square, description: `Pin: ${pieceType.toUpperCase()} pins ${firstPiece.piece.type.toUpperCase()} to king` }; } break; }
}
f += dx; r += dy;
}
}
return null;
},
analyzeSafety(fen, uci, ourColor, config) {
const failResult = (msg) => ({ safe: false, riskScore: 999, warnings: [msg], riskLevel: 100 });
if (!fen || !uci || uci.length < 4) return failResult("Invalid input");
if (!ourColor || (ourColor !== "w" && ourColor !== "b")) return failResult("Invalid color");
const from = uci.slice(0, 2), to = uci.slice(2, 4), promo = uci.length > 4 ? uci[4] : null;
const piece = pieceFromFenChar(fenCharAtSquare(fen, from));
if (!piece) return failResult("No piece found");
if (piece.color !== ourColor) return failResult("Not our piece");
const newFen = makeSimpleMove(fen, from, to, promo);
if (!newFen) return failResult("Invalid move");
const oppColor = ourColor === "w" ? "b" : "w";
let riskScore = 0; const warnings = [];
const ourKing = findKing(newFen, ourColor);
if (ourKing && isSquareAttackedBy(newFen, ourKing, oppColor)) { return { safe: false, riskScore: 1000, warnings: ["King exposed - ILLEGAL"], riskLevel: 100 }; }
const ourKingBefore = findKing(fen, ourColor);
if (ourKingBefore && piece.type !== 'k') {
if (isPiecePinned(fen, from, ourKingBefore, ourColor, oppColor)) {
const kingAfter = findKing(newFen, ourColor);
if (kingAfter && isSquareAttackedBy(newFen, kingAfter, oppColor)) { return { safe: false, riskScore: 1000, warnings: ["Moving pinned piece - ILLEGAL"], riskLevel: 100 }; }
riskScore += this.RISK_MULTIPLIERS.PIN_PENALTY; warnings.push("Moving previously pinned piece");
}
}
const attackers = getAttackersOfSquare(newFen, to, oppColor);
const defenders = getAttackersOfSquare(newFen, to, ourColor);
const capturedCh = fenCharAtSquare(fen, to);
const capturedPiece = pieceFromFenChar(capturedCh);
const capturedVal = (capturedPiece && capturedPiece.color === oppColor) ? (PIECE_VALUES[capturedPiece.type] || 0) : 0;
if (attackers.length > 0) {
const pieceVal = PIECE_VALUES[piece.type] || 0;
const minAtkVal = Math.min(...attackers.map(a => PIECE_VALUES[a.piece] || 99));
if (defenders.length === 0) { const netLoss = pieceVal - capturedVal; if (netLoss > 0) { riskScore += netLoss * this.RISK_MULTIPLIERS.PIECE_HANGING; warnings.push(`${piece.type.toUpperCase()} hangs (net loss: ${netLoss})`); } }
else if (minAtkVal < pieceVal && capturedVal < pieceVal) { const netLoss = pieceVal - Math.max(capturedVal, minAtkVal); if (netLoss > 0) { riskScore += netLoss * this.RISK_MULTIPLIERS.BAD_TRADE; warnings.push(`Bad trade: ${piece.type.toUpperCase()} (net: -${netLoss})`); } }
}
const oppLongRange = getAllPieces(newFen, oppColor).filter(p => ['q', 'r', 'b'].includes(p.type));
for (const oppPiece of oppLongRange) {
const ourValuable = getAllPieces(newFen, ourColor).filter(p => (PIECE_VALUES[p.type] || 0) >= 5 || p.type === 'k');
for (const target of ourValuable) {
const attackedNow = canPieceAttackSquare(newFen, oppPiece, target.square);
const attackedBefore = canPieceAttackSquare(fen, oppPiece, target.square);
if (attackedNow && !attackedBefore) {
riskScore += this.RISK_MULTIPLIERS.DISCOVERED_CHECK_PENALTY; warnings.push(`Discovered attack on our ${target.type.toUpperCase()} by ${oppPiece.type.toUpperCase()}`);
if (target.type === 'k') { riskScore += 100; warnings.push("DISCOVERED CHECK against our king!"); }
}
}
}
const leftBehind = this._checkLeftBehindPieces(fen, newFen, from, to, ourColor, oppColor);
if (leftBehind.totalRisk > 0) { riskScore += leftBehind.totalRisk; warnings.push(...leftBehind.warnings); }
if (piece.type === 'k') { const kingSafety = this._evaluateKingSafety(newFen, to, ourColor, oppColor); riskScore += kingSafety.risk; warnings.push(...kingSafety.warnings); }
if (piece.type === 'q') { const queenTrapped = this._isQueenTrapped(newFen, to, ourColor, oppColor); if (queenTrapped) { riskScore += 80; warnings.push("QUEEN may be trapped!"); } }
const maxRisk = config.maxRiskScore || 500;
const safeThreshold = config.riskTolerance * this.RISK_MULTIPLIERS.SAFE_THRESHOLD;
const safe = riskScore <= safeThreshold;
return { safe, riskScore: Math.min(riskScore, maxRisk), warnings, riskLevel: Math.min(100, riskScore / this.RISK_MULTIPLIERS.RISK_LEVEL_DIVISOR) };
},
_checkLeftBehindPieces(oldFen, newFen, from, to, ourColor, oppColor) {
let totalRisk = 0; const warnings = [];
const ourPieces = getAllPieces(oldFen, ourColor);
for (const p of ourPieces) {
if (p.square === from || p.type === 'k') continue;
const wasDefendedByUs = isSquareDefendedBy(oldFen, p.square, from);
if (!wasDefendedByUs) continue;
const stillDefendedByMoved = isSquareDefendedBy(newFen, p.square, to);
const otherDefenders = getAttackersOfSquare(newFen, p.square, ourColor);
if (stillDefendedByMoved || otherDefenders.length > 0) continue;
const atk = getAttackersOfSquare(newFen, p.square, oppColor);
if (atk.length > 0) { const val = PIECE_VALUES[p.type] || 0; totalRisk += val * 8; warnings.push(`${p.type.toUpperCase()} at ${p.square} left undefended and attacked`); }
}
return { totalRisk, warnings };
},
_evaluateKingSafety(fen, kingSquare, ourColor, oppColor) {
let risk = 0; const warnings = [];
const kf = "abcdefgh".indexOf(kingSquare[0]), kr = parseInt(kingSquare[1], 10);
let unsafeNeighbors = 0;
for (let df = -1; df <= 1; df++) { for (let dr = -1; dr <= 1; dr++) { if (df === 0 && dr === 0) continue; const nf = kf + df, nr = kr + dr; if (nf < 0 || nf > 7 || nr < 1 || nr > 8) continue; if (isSquareAttackedBy(fen, "abcdefgh"[nf] + nr, oppColor)) unsafeNeighbors++; } }
if (unsafeNeighbors >= 5) { risk += this.RISK_MULTIPLIERS.KING_SAFETY_WEIGHT * 2; warnings.push("King destination very exposed"); }
else if (unsafeNeighbors >= 3) { risk += this.RISK_MULTIPLIERS.KING_SAFETY_WEIGHT; warnings.push("King destination somewhat exposed"); }
return { risk, warnings };
},
_isQueenTrapped(fen, queenSquare, ourColor, oppColor) {
const escapes = getQueenEscapeSquares(fen, queenSquare, ourColor);
const safeEscapes = escapes.filter(sq => { const ch = fenCharAtSquare(fen, sq); if (ch) { const p = pieceFromFenChar(ch); if (p && p.color === ourColor) return false; } return !isSquareAttackedBy(fen, sq, oppColor); });
return safeEscapes.length <= 1;
},
_validateCaptureQuality(fen, uci, ourColor, config) {
const from = uci.substring(0, 2), to = uci.substring(2, 4);
if (isEnPassantCapture(fen, from, to, ourColor)) return { ok: true };
const movingPiece = pieceFromFenChar(fenCharAtSquare(fen, from));
const capturedPiece = pieceFromFenChar(fenCharAtSquare(fen, to));
if (!capturedPiece) return { ok: false, reason: "Tidak ada piece yang diambil" };
const capturedVal = PIECE_VALUES[capturedPiece.type] || 0;
const ourVal = PIECE_VALUES[movingPiece ? movingPiece.type : 'p'] || 0;
if (config.minCaptureValue && capturedVal < config.minCaptureValue) return { ok: false, reason: `Capture terlalu murah: ${capturedVal} < min ${config.minCaptureValue}` };
if (config.requirePositiveExchange) { const see = this._staticExchangeEval(fen, from, to, ourColor); if (see < (config.minSEEValue || 0)) return { ok: false, reason: `Exchange tidak menguntungkan (SEE: ${see})` }; }
if (config.avoidLossyCapture && movingPiece) { const oppColor = ourColor === "w" ? "b" : "w"; const newFen = makeSimpleMove(fen, from, to); if (newFen) { const atk = getAttackersOfSquare(newFen, to, oppColor); const def = getAttackersOfSquare(newFen, to, ourColor); if (atk.length > 0 && def.length === 0) { const netLoss = ourVal - capturedVal; if (netLoss > (config.maxNetLoss !== undefined ? config.maxNetLoss : 0)) return { ok: false, reason: `Capture berisiko: net loss ${netLoss}` }; } } }
return { ok: true };
},
_validateMoveQuality(fen, uci, ourColor, tactical, config) {
const from = uci.substring(0, 2), to = uci.substring(2, 4);
const movingPiece = pieceFromFenChar(fenCharAtSquare(fen, from));
if (!movingPiece) return { ok: false, reason: "Piece tidak ditemukan" };
const oppColor = ourColor === "w" ? "b" : "w";
if (tactical.isBrilliant) return { ok: true };
if (tactical.score >= 5) return { ok: true };
const minScore = config.minTacticalScore !== undefined ? config.minTacticalScore : -3;
if (tactical.score < minScore) return { ok: false, reason: `Taktik lemah: score ${tactical.score} < minimum ${minScore}` };
const newFen = makeSimpleMove(fen, from, to);
if (!newFen) return { ok: true };
const atk = getAttackersOfSquare(newFen, to, oppColor);
const def = getAttackersOfSquare(newFen, to, ourColor);
const val = PIECE_VALUES[movingPiece.type] || 0;
if (val >= 5 && atk.length > 0 && def.length === 0) return { ok: false, reason: `${movingPiece.type.toUpperCase()} digantung di ${to}` };
if (val >= 3 && atk.length > 0 && def.length === 0) { const minAtk = Math.min(...atk.map(a => PIECE_VALUES[a.piece] || 99)); if (minAtk < val) return { ok: false, reason: `${movingPiece.type.toUpperCase()} diserang piece lebih murah di ${to}` }; }
const ourKing = findKing(fen, ourColor);
if (ourKing && movingPiece.type !== 'k' && isPiecePinned(fen, from, ourKing, ourColor, oppColor)) return { ok: false, reason: "Move melepas pin - berbahaya" };
return { ok: true };
},
calculateConfidence(scoreInfo, tactical, safety, config) {
let score = 50;
if (scoreInfo) {
if (scoreInfo.type === "mate") { const mate = scoreInfo.value, dist = Math.abs(mate); if (mate < 0) { if (dist <= 1) score += 35; else if (dist <= 2) score += 28; else if (dist <= 4) score += 18; else if (dist <= 6) score += 10; else score += 5; } else { if (dist <= 1) score -= 50; else if (dist <= 2) score -= 35; else if (dist <= 4) score -= 20; else score -= 10; } }
else { const ourAdv = -(scoreInfo.value || 0) / 100; if (ourAdv >= 8) score += 28; else if (ourAdv >= 5) score += 22; else if (ourAdv >= 3) score += 16; else if (ourAdv >= 1.5) score += 10; else if (ourAdv >= 0.5) score += 5; else if (ourAdv >= 0) score += 0; else if (ourAdv >= -1) score -= 8; else if (ourAdv >= -2) score -= 16; else if (ourAdv >= -4) score -= 24; else score -= 32; }
}
if (tactical.isBlunder) score -= 60; else if (tactical.isBrilliant) score += 18;
if (tactical.score > 0) score += Math.min(config.tacticalBonus || 10, tactical.score * 2); else if (tactical.score < 0) score += Math.max(-30, tactical.score * 3);
if (safety.riskScore > 0) { const risk = safety.riskScore; let penalty; if (risk <= 20) penalty = risk * 0.3; else if (risk <= 50) penalty = 6 + (risk - 20) * 0.6; else if (risk <= 100) penalty = 24 + (risk - 50) * 0.8; else penalty = 64 + (risk - 100) * 1.0; score -= Math.min(45, penalty); }
const warnCount = safety.warnings ? safety.warnings.length : 0;
if (warnCount >= 4) score -= 10; else if (warnCount >= 2) score -= 4;
if (this.isInCautiousMode()) score -= 18;
return Math.max(3, Math.min(95, Math.round(score)));
},
_isCaptureMove(fen, uci, ourColor) { if (!fen || !uci || uci.length < 4 || !ourColor) return false; const from = uci.substring(0, 2), to = uci.substring(2, 4); const target = pieceFromFenChar(fenCharAtSquare(fen, to)); if (target && target.color !== ourColor) return true; return isEnPassantCapture(fen, from, to, ourColor); },
_isGoodCapture(fen, uci, ourColor) { if (!this._isCaptureMove(fen, uci, ourColor)) return false; const from = uci.substring(0, 2), to = uci.substring(2, 4); return this._staticExchangeEval(fen, from, to, ourColor) >= 0; },
_multiPvConvergenceCheck(fen, uci, pvMoves, ourColor) { const result = { converged: false, validIn: 0, totalChecked: 0, bonus: 0 }; const oppColor = ourColor === "w" ? "b" : "w"; const oppCandidates = new Set(); if (pvMoves && pvMoves[0]) oppCandidates.add(pvMoves[0]); const fenHash = hashFen(fen); const bucket = (Engine && Engine._premoveCandidates) ? Engine._premoveCandidates[fenHash] : null; if (Array.isArray(bucket)) { bucket.forEach(c => { if (c.pvMoves && c.pvMoves[0]) oppCandidates.add(c.pvMoves[0]); }); } const from = uci.substring(0, 2), to = uci.substring(2, 4), promo = uci[4]; for (const oppMove of oppCandidates) { if (!oppMove || oppMove.length < 4) continue; result.totalChecked++; const predFen = makeSimpleMove(fen, oppMove.slice(0, 2), oppMove.slice(2, 4), oppMove[4]); if (!predFen) continue; const piece = pieceFromFenChar(fenCharAtSquare(predFen, from)); if (!piece || piece.color !== ourColor) continue; const afterFen = makeSimpleMove(predFen, from, to, promo); if (!afterFen) continue; const king = findKing(afterFen, ourColor); if (king && isSquareAttackedBy(afterFen, king, oppColor)) continue; const atk = getAttackersOfSquare(afterFen, to, oppColor); const def = getAttackersOfSquare(afterFen, to, ourColor); const pVal = PIECE_VALUES[piece.type] || 0; if (atk.length > 0 && def.length === 0 && pVal >= 5) continue; result.validIn++; } const ratio = result.totalChecked > 0 ? result.validIn / result.totalChecked : 0; if (ratio >= 1.0) result.bonus = this.HEURISTICS.MULTIPV_FULL_CONSENSUS; else if (ratio >= 0.6) result.bonus = this.HEURISTICS.MULTIPV_PARTIAL_CONSENSUS; else if (ratio < 0.4) result.bonus = this.HEURISTICS.MULTIPV_DIVERGENT_PENALTY; result.converged = ratio >= 0.6; return result; },
_analyzeRecapture(predictedFen, uci, ourColor, pvMoves) { const result = { isRecapture: false, bonus: 0, speedMultiplier: 1.0 }; let oppMoveTo = null; if (pvMoves && pvMoves[0] && pvMoves[0].length >= 4) oppMoveTo = pvMoves[0].substring(2, 4); if (!oppMoveTo) { const history = getGameHistory(); if (history.length > 0 && history[history.length - 1].length >= 4) oppMoveTo = history[history.length - 1].substring(2, 4); } if (!oppMoveTo) return result; const myTo = uci.substring(2, 4); if (oppMoveTo !== myTo) return result; const target = pieceFromFenChar(fenCharAtSquare(predictedFen, myTo)); if (!target || target.color === ourColor) return result; const see = this._staticExchangeEval(predictedFen, uci.substring(0, 2), myTo, ourColor); result.isRecapture = true; if (see >= 0) { result.bonus = 35; result.speedMultiplier = 0.5; } else if (see >= -2) { result.bonus = 10; result.speedMultiplier = 0.7; } else { result.bonus = -10; } return result; },
_detectForcedResponse(fen, ourColor) { const result = { isForced: false, bonus: 0 }; const oppColor = ourColor === "w" ? "b" : "w"; const pieces = getAllPieces(fen, oppColor); let reasonableMoves = 0; const oppKing = findKing(fen, oppColor); const weGiveCheck = oppKing && isSquareAttackedBy(fen, oppKing, ourColor); const checkEscapeMoves = []; for (const p of pieces) { const squares = getSquaresAttackedByPiece(fen, p.square, p.type, oppColor); for (const sq of squares) { const testFen = makeSimpleMove(fen, p.square, sq); if (!testFen) continue; const kingAfter = findKing(testFen, oppColor); if (kingAfter && !isSquareAttackedBy(testFen, kingAfter, ourColor)) { reasonableMoves++; if (weGiveCheck) checkEscapeMoves.push(p.square + sq); } if (reasonableMoves > 5) break; } if (reasonableMoves > 5) break; } if (weGiveCheck) { if (checkEscapeMoves.length === 1) { result.bonus = 50; result.isForced = true; } else if (checkEscapeMoves.length <= 3) { result.bonus = 30; result.isForced = true; } else { result.bonus = 12; } } else { if (reasonableMoves <= 1) result.bonus = 30; else if (reasonableMoves <= 3) result.bonus = 15; result.isForced = reasonableMoves <= 3; } return result; },
_getTimePressureBonus() { try { const clock = getClockTimes(); if (!clock || !clock.found) return { bonus: 0, speedMultiplier: 1.0 }; const myTime = clock.playerTime, oppTime = clock.opponentTime; let bonus = 0, speedMult = 1.0; if (myTime !== null && myTime < 10) { bonus += 20; speedMult = 0.3; } else if (myTime !== null && myTime < 30) { bonus += 8; speedMult = 0.6; } if (oppTime !== null && oppTime < 10) { bonus += 10; speedMult = Math.min(speedMult, 0.5); } return { bonus, speedMultiplier: speedMult }; } catch (e) { return { bonus: 0, speedMultiplier: 1.0 }; } },
_checkBookPremove(fen, ourColor) { if (!State.useOpeningBook || State.moveNumber > 12) return null; const history = getGameHistory(); const bookMove = OpeningBook.getMove(fen, history); if (bookMove && bookMove.length >= 4) return bookMove; return null; },
_checkEndgamePremove(fen) { const fenHash = hashFen(fen); if (Syzygy && Syzygy.cache && Syzygy.cache.has(fenHash)) { const data = Syzygy.cache.get(fenHash).payload; if (data && data.moves && data.moves.length > 0) { const best = data.moves[0]; if (best && best.category !== "loss") return best.uci; } } return null; },
_detectSacrifice(fen, predictedFen, uci, ourColor, pvMoves) { const result = { isSacrifice: false, penalty: 0, details: "" }; const oppMove = (pvMoves && pvMoves[0] && pvMoves[0].length >= 4) ? pvMoves[0] : null; if (!oppMove) return result; const oppFrom = oppMove.substring(0, 2), oppTo = oppMove.substring(2, 4); const capturedByOpp = pieceFromFenChar(fenCharAtSquare(fen, oppTo)); if (!capturedByOpp || capturedByOpp.color !== ourColor) return result; const oppColor = ourColor === "w" ? "b" : "w"; const oppSee = this._staticExchangeEval(fen, oppFrom, oppTo, oppColor); if (oppSee < 0) { result.isSacrifice = true; const sacrificeDepth = Math.abs(oppSee); result.details = `Opp may sacrifice ${capturedByOpp.type} (SEE: ${oppSee})`; if (sacrificeDepth >= 5) { result.penalty = 25; } else if (sacrificeDepth >= 3) { result.penalty = 15; } else { result.penalty = 5; } } return result; },
_premoveToken: 0,
_newToken: function () { this._premoveToken = (this._premoveToken || 0) + 1; return this._premoveToken; },
_isCurrentToken: function (token) { return token === this._premoveToken; },
_invalidateAllTokens: function () { this._premoveToken = (this._premoveToken || 0) + 1; },
HEURISTICS: { CHECK_MATE_BONUS: 60, SINGLE_ESCAPE_BONUS: 50, FORCED_MOVE_BONUS_HIGH: 30, FORCED_MOVE_BONUS_LOW: 15, RECAPTURE_SAFE_BONUS: 35, RECAPTURE_MARGINAL_BONUS: 10, RECAPTURE_BAD_PENALTY: -10, MULTIPV_FULL_CONSENSUS: 25, MULTIPV_PARTIAL_CONSENSUS: 12, MULTIPV_DIVERGENT_PENALTY: -15, PV_CONSENSUS_MAX: 30, TIME_PRESSURE_CRITICAL: 25, TIME_PRESSURE_NORMAL: 12, OPPONENT_LOW_TIME_BONUS: 10, SACRIFICE_QUEEN_PENALTY: 25, SACRIFICE_PIECE_PENALTY: 15, SACRIFICE_MINOR_PENALTY: 5, VOLATILITY_HIGH_PENALTY: 5, TACTICAL_EXPLOSION_MAX: 10, REPLY_TREE_BAD: 3, VOLATILITY_BLOCK: 95, EXPLOSION_BLOCK: 80, RISK_CRITICAL: 1000, DELAY_RECAPTURE: 0.35, DELAY_FORCED: 0.5, DELAY_CONVERGED: 0.7, DELAY_SACRIFICE: 2.2, DELAY_VOLATILE: 1.6, DELAY_BOOK_TB: 0.35, DELAY_MIN_MS: 150, DELAY_MAX_MS: 2000, TC_BULLET_BOOST: 30, TC_BLITZ_BOOST: 15, TC_CLASSICAL_PENALTY: -10 },
_calculatePositionVolatility(fen) { let score = 0; const parts = fen.split(" "); const placement = parts[0]; const turn = parts[1] || "w"; const oppColor = turn === "w" ? "b" : "w"; const pieceMatch = placement.match(/[pnbrqkPNBRQK]/g); score += (pieceMatch ? pieceMatch.length : 0) * 3; const ourKing = findKing(fen, turn); if (ourKing && isSquareAttackedBy(fen, ourKing, oppColor)) score += 40; const oppKing = findKing(fen, oppColor); if (oppKing && isSquareAttackedBy(fen, oppKing, turn)) score += 25; const ourPieces = getAllPieces(fen, turn); let hangingCount = 0; for (const p of ourPieces) { if (p.type === "k") continue; const atk = getAttackersOfSquare(fen, p.square, oppColor).length; const def = getAttackersOfSquare(fen, p.square, turn).length; if (atk > 0 && def === 0) hangingCount += PIECE_VALUES[p.type] || 0; } score += hangingCount * 10; let captureOpportunities = 0; for (const p of ourPieces) { const sqs = getSquaresAttackedByPiece(fen, p.square, p.type, turn); for (const s of sqs) { const target = pieceFromFenChar(fenCharAtSquare(fen, s)); if (target && target.color === oppColor) captureOpportunities++; } } score += Math.min(captureOpportunities, 10) * 2; let totalMoves = 0; for (const p of ourPieces) { totalMoves += getSquaresAttackedByPiece(fen, p.square, p.type, turn).length; } score += Math.min(totalMoves / 5, 20); return Math.min(100, Math.round(score)); },
_calculatePvConsensus(fen, ourUci, ourColor) { const result = { consensus: 0, bonus: 0, totalLines: 0, agreedLines: 0 }; const fenHash = hashFen(fen); const bucket = (Engine && Engine._premoveCandidates) ? Engine._premoveCandidates[fenHash] : null; if (!Array.isArray(bucket) || bucket.length < 2) return result; result.totalLines = bucket.length; for (const c of bucket) { if (!c.pvMoves || c.pvMoves.length < 2) continue; if (c.pvMoves[1] === ourUci) result.agreedLines++; } result.consensus = result.totalLines > 0 ? result.agreedLines / result.totalLines : 0; if (result.consensus >= 1.0) result.bonus = this.HEURISTICS.PV_CONSENSUS_MAX; else if (result.consensus >= 0.8) result.bonus = Math.round(this.HEURISTICS.PV_CONSENSUS_MAX * 0.6); else if (result.consensus >= 0.5) result.bonus = Math.round(this.HEURISTICS.PV_CONSENSUS_MAX * 0.27); return result; },
_opponentReplyExpectedScore(fen, ourUci, ourColor, pvMoves) { const result = { expectedScore: 0, worstScore: 999, bestScore: -999, replies: 0 }; const oppColor = ourColor === "w" ? "b" : "w"; const oppCandidates = []; const fenHash = hashFen(fen); const bucket = (Engine && Engine._premoveCandidates) ? Engine._premoveCandidates[fenHash] : null; if (Array.isArray(bucket)) { for (const c of bucket) { if (c.pvMoves && c.pvMoves[0] && c.pvMoves[0].length >= 4) oppCandidates.push({ move: c.pvMoves[0], weight: 1 }); } } if (pvMoves && pvMoves[0] && pvMoves[0].length >= 4 && !oppCandidates.some(c => c.move === pvMoves[0])) oppCandidates.push({ move: pvMoves[0], weight: 2 }); if (oppCandidates.length === 0) return result; const totalWeight = oppCandidates.reduce((s, c) => s + c.weight, 0); const ourFrom = ourUci.substring(0, 2), ourTo = ourUci.substring(2, 4), ourPromo = ourUci.length > 4 ? ourUci[4] : null; for (const cand of oppCandidates) { const predFen = makeSimpleMove(fen, cand.move.substring(0, 2), cand.move.substring(2, 4)); if (!predFen) continue; const piece = pieceFromFenChar(fenCharAtSquare(predFen, ourFrom)); if (!piece || piece.color !== ourColor) { result.worstScore = -999; continue; } const afterFen = makeSimpleMove(predFen, ourFrom, ourTo, ourPromo); if (!afterFen) { result.worstScore = -999; continue; } const king = findKing(afterFen, ourColor); if (king && isSquareAttackedBy(afterFen, king, oppColor)) { result.worstScore = -999; continue; } const atk = getAttackersOfSquare(afterFen, ourTo, oppColor); const def = getAttackersOfSquare(afterFen, ourTo, ourColor); const pVal = PIECE_VALUES[piece.type] || 0; let replyScore = 0; if (atk.length > 0 && def.length === 0) replyScore = -pVal * 3; else if (atk.length > def.length) replyScore = -pVal; else replyScore = 1; const prob = cand.weight / totalWeight; result.expectedScore += prob * replyScore; if (replyScore < result.worstScore) result.worstScore = replyScore; if (replyScore > result.bestScore) result.bestScore = replyScore; result.replies++; } return result; },
_detectTacticalExplosion(fen, ourColor) { const result = { explosion: false, severity: 0, reasons: [] }; const oppColor = ourColor === "w" ? "b" : "w"; let severity = 0; const ourKing = findKing(fen, ourColor); if (ourKing && isSquareAttackedBy(fen, ourKing, oppColor)) { severity += 60; result.reasons.push("Our king in check"); } const oppKing = findKing(fen, oppColor); if (oppKing && isSquareAttackedBy(fen, oppKing, ourColor)) { severity += 30; result.reasons.push("We give check"); } const ourPieces = getAllPieces(fen, ourColor); let hangingVal = 0; for (const p of ourPieces) { if (p.type === "k") continue; const atk = getAttackersOfSquare(fen, p.square, oppColor).length; const def = getAttackersOfSquare(fen, p.square, ourColor).length; if (atk > 0 && def === 0) hangingVal += PIECE_VALUES[p.type] || 0; } if (hangingVal >= 9) { severity += 40; result.reasons.push("Queen/rook hanging"); } else if (hangingVal >= 5) { severity += 20; result.reasons.push("Major piece hanging"); } if (ThreatDetectionSystem && ThreatDetectionSystem.detectOpponentForks) { const forks = ThreatDetectionSystem.detectOpponentForks(fen, ourColor); if (Array.isArray(forks) && forks.length > 0) { severity += 25; result.reasons.push("Fork threat detected"); } } result.severity = Math.min(100, severity); result.explosion = severity >= 40; return result; },
_getContextualPremoveDelay(decision) { let base = State.premoveDelayMs || 250; if (decision.recapture && decision.recapture.isRecapture) base = Math.round(base * this.HEURISTICS.DELAY_RECAPTURE); else if (decision.forced && decision.forced.isForced) base = Math.round(base * this.HEURISTICS.DELAY_FORCED); else if (decision.convergence && decision.convergence.converged) base = Math.round(base * this.HEURISTICS.DELAY_CONVERGED); if (decision.sacrifice && decision.sacrifice.isSacrifice) base = Math.round(base * this.HEURISTICS.DELAY_SACRIFICE); if (decision.volatility && decision.volatility > 60) base = Math.round(base * this.HEURISTICS.DELAY_VOLATILE); if (decision.timePressure && decision.timePressure.speedMultiplier < 1) base = Math.round(base * decision.timePressure.speedMultiplier); if (decision.isBook || decision.isTB) base = Math.round(base * this.HEURISTICS.DELAY_BOOK_TB); return this._humanDelay(Math.max(60, Math.min(2000, base))); },
_getTimeControlProfile() { const clock = getClockTimes(); if (!clock.found || clock.playerTime === null) return { profile: "rapid", aggressionMultiplier: 1.0, confidenceBoost: 0 }; const initialEstimate = clock.playerTime + (State.moveNumber || 1) * 5; if (initialEstimate <= 180) return { profile: "bullet", aggressionMultiplier: 2.2, confidenceBoost: this.HEURISTICS.TC_BULLET_BOOST }; else if (initialEstimate <= 600) return { profile: "blitz", aggressionMultiplier: 1.5, confidenceBoost: this.HEURISTICS.TC_BLITZ_BOOST }; else if (initialEstimate <= 1800) return { profile: "rapid", aggressionMultiplier: 1.0, confidenceBoost: 0 }; else return { profile: "classical", aggressionMultiplier: 0.6, confidenceBoost: this.HEURISTICS.TC_CLASSICAL_PENALTY }; },
_validatePreExecution(fenHash, uci, ourColor, isPremove) { const currentFen = getAccurateFen(); if (!currentFen) return { ok: false, reason: "Cannot read board" }; const from = uci.substring(0, 2); const piece = pieceFromFenChar(fenCharAtSquare(currentFen, from)); if (!piece || piece.color !== ourColor) return { ok: false, reason: "Piece missing from origin" }; const turn = getCurrentTurn(currentFen); if (!isPremove && turn !== ourColor) return { ok: false, reason: "Not our turn" }; if (!isPremove) { const to = uci.substring(2, 4), promo = uci.length > 4 ? uci[4] : null; const afterFen = makeSimpleMove(currentFen, from, to, promo); if (!afterFen || afterFen === currentFen) return { ok: false, reason: "Move illegal" }; const oppColor = ourColor === "w" ? "b" : "w"; const king = findKing(afterFen, ourColor); if (king && isSquareAttackedBy(afterFen, king, oppColor)) return { ok: false, reason: "Move exposes king" }; } const board = getBoardElement(); if (board && (board.classList.contains("animating") || board.classList.contains("dragging"))) return { ok: false, reason: "Board is animating/dragging" }; return { ok: true }; },
shouldPremove(fen, uci, pvMoves, scoreInfo) {
if (this.executionLock || this.processingLock) return { allowed: false, reason: "System locked" };
if (this.isInErrorCooldown()) return { allowed: false, reason: "Error cooldown active" };
const ourColor = getPlayingAs();
if (!ourColor) return { allowed: false, reason: "Unknown color" };
const predictedFen = getPredictedFen(fen, pvMoves);
if (predictedFen) { const bookMove = this._checkBookPremove(predictedFen, ourColor); if (bookMove === uci) return { allowed: true, confidence: 95, reason: "Theory/Book", isBook: true }; const tbMove = this._checkEndgamePremove(predictedFen); if (tbMove === uci) return { allowed: true, confidence: 100, reason: "Tablebase (Win/Draw)", isTB: true }; }
if (!predictedFen || predictedFen === fen) return { allowed: false, reason: "Cannot predict position after opponent move" };
const fenHash = hashFen(predictedFen);
if (this.executedFens.has(fenHash)) return { allowed: false, reason: "Position already executed" };
const from = uci.substring(0, 2), to = uci.substring(2, 4);
const movingPiece = pieceFromFenChar(fenCharAtSquare(predictedFen, from));
if (!movingPiece || movingPiece.color !== ourColor) return { allowed: false, reason: "Move invalid in predicted position" };
const config = this.AGGRESSION_CONFIG[State.premoveMode] || this.AGGRESSION_CONFIG.filter;
const mode = State.premoveMode || "filter";
const isEveryMode = mode === "every", isCaptureMode = mode === "capture", isFilteredMode = mode === "filter";
const recapture = this._analyzeRecapture(predictedFen, uci, ourColor, pvMoves);
const forced = this._detectForcedResponse(fen, ourColor);
const convergence = this._multiPvConvergenceCheck(fen, uci, pvMoves, ourColor);
const timePressure = this._getTimePressureBonus();
const sacrifice = this._detectSacrifice(fen, predictedFen, uci, ourColor, pvMoves);
const volatility = this._calculatePositionVolatility(fen);
const pvConsensus = this._calculatePvConsensus(fen, uci, ourColor);
const replyTree = this._opponentReplyExpectedScore(fen, uci, ourColor, pvMoves);
const tacticalExplosion = this._detectTacticalExplosion(fen, ourColor);
const tcProfile = this._getTimeControlProfile();
if (isCaptureMode) { if (!this._isCaptureMove(predictedFen, uci, ourColor)) return { allowed: false, reason: "Capture mode: bukan capture" }; const captureCheck = this._validateCaptureQuality(predictedFen, uci, ourColor, config); if (!captureCheck.ok) return { allowed: false, reason: "Capture mode: " + captureCheck.reason }; }
if (isEveryMode && config.allowSpeculative && scoreInfo && scoreInfo.type === "cp") { const evalForUs = -(scoreInfo.value || 0); if (evalForUs < (config.minEvalForSpeculative || -200)) return { allowed: false, reason: `Every mode: eval terlalu negatif (${evalForUs}cp)` }; }
const tactical = this.analyzeTacticalMotifs(predictedFen, uci, ourColor);
if (tactical.isBlunder) { this.recordBlunder(predictedFen); return { allowed: false, reason: "Blunder terdeteksi: " + tactical.details.join("; "), tactical }; }
if (isFilteredMode && config.requireMoveQuality) { const moveCheck = this._validateMoveQuality(predictedFen, uci, ourColor, tactical, config); if (!moveCheck.ok) return { allowed: false, reason: "Filter mode: " + moveCheck.reason }; }
const safety = this.analyzeSafety(predictedFen, uci, ourColor, config);
if (safety.riskScore >= 1000) return { allowed: false, reason: "Safety critical: " + (safety.warnings[0] || "King exposed"), safety };
if (safety.riskScore >= (config.maxRiskScore || 200)) return { allowed: false, reason: `Risk ${Math.round(safety.riskScore)} >= cap ${config.maxRiskScore}`, safety };
const riskBoost = isEveryMode ? 1.4 : (isCaptureMode ? 0.7 : 1.0);
const riskThreshold = config.riskTolerance * this.RISK_MULTIPLIERS.BLOCK_THRESHOLD * riskBoost;
if (!safety.safe && safety.riskScore > riskThreshold) return { allowed: false, reason: `Terlalu berisiko: ${Math.round(safety.riskScore)} > ${Math.round(riskThreshold)}`, safety };
if (isCaptureMode && (!safety.safe || safety.riskScore > (config.maxRiskScore || 100))) return { allowed: false, reason: "Capture mode: capture tidak aman", riskScore: safety.riskScore };
let confidence = this.calculateConfidence(scoreInfo, tactical, safety, config);
if (recapture.isRecapture) confidence += recapture.bonus;
if (forced.isForced) confidence += forced.bonus;
confidence += convergence.bonus; confidence += timePressure.bonus;
if (sacrifice.isSacrifice) confidence -= sacrifice.penalty;
confidence += pvConsensus.bonus; confidence += tcProfile.confidenceBoost;
if (tacticalExplosion.explosion) confidence -= Math.min(tacticalExplosion.severity / 5, 10);
if (volatility > 80) confidence -= 5;
if (replyTree.replies > 0 && replyTree.worstScore < -5) confidence -= 3;
if (isEveryMode) { const safetyRatio = Math.max(0, 1 - (safety.riskScore / (config.maxRiskScore || 220))); confidence = Math.min(95, confidence + Math.round((config.maxConfidenceBoost || 8) * safetyRatio)); }
if (isCaptureMode) { confidence = Math.max(5, confidence - 3); if (tactical.score >= 3) confidence = Math.min(95, confidence + 5); }
if (isFilteredMode && tactical.isBrilliant) confidence = Math.min(90, confidence + 8);
if (this.isInCautiousMode()) confidence = Math.max(config.minConfidence, confidence - 20);
const effectiveMin = this.isInCautiousMode() ? Math.min(60, config.minConfidence + 20) : config.minConfidence;
if (confidence < effectiveMin) return { allowed: false, reason: `Confidence kurang: ${confidence} < ${effectiveMin}`, confidence, required: effectiveMin };
const pattern = this.detectPattern();
if (pattern && pattern.isTooConsistent && !isEveryMode) { const threshold = 68 + Math.random() * 22; if (confidence < threshold && !tactical.isBrilliant) { this.patternBreakCounter++; return { allowed: false, reason: "Pattern break (humanisasi)", pattern }; } }
if (!tactical.isBrilliant) { const roll = Math.random() * 100; const rollBuffer = config.rollBuffer || 0; if (roll > confidence + rollBuffer) return { allowed: false, reason: `Roll gagal (roll: ${Math.round(roll)}, need: ${Math.round(confidence + rollBuffer)})`, confidence, roll: Math.round(roll) }; }
const promo = uci.length > 4 ? uci[4] : null;
const verifyFen = makeSimpleMove(predictedFen, from, to, promo);
if (verifyFen) { const oppColor = ourColor === "w" ? "b" : "w"; const verifyKing = findKing(verifyFen, ourColor); if (verifyKing && isSquareAttackedBy(verifyFen, verifyKing, oppColor)) return { allowed: false, reason: "Final gate: move meninggalkan raja dalam check" }; }
return { allowed: true, confidence, tactical, safety, pattern, mode, recapture, forced, convergence, timePressure, sacrifice, volatility, pvConsensus, replyTree, tacticalExplosion, tcProfile };
},
async execute(fen, uci, decision) {
if (!decision || !decision.allowed) return false;
const now = Date.now();
if (now - this.lastExecutionTime < this.MIN_EXECUTION_INTERVAL) return false;
if (this.executionLock) return false;
const execToken = this._newToken();
this.executionLock = true; this.processingLock = true;
try {
const fenHash = hashFen(fen);
if (this.executedFens.has(fenHash)) return false;
this.executedFens.add(fenHash);
if (this.executedFens.size > this.MAX_EXECUTED_FENS) { const arr = [...this.executedFens]; this.executedFens = new Set(arr.slice(-Math.floor(this.MAX_EXECUTED_FENS * 0.7))); }
const delay = this._getContextualPremoveDelay(decision);
await sleep(delay);
if (!this._isCurrentToken(execToken)) { this.executedFens.delete(fenHash); return false; }
const currentFen = getAccurateFen();
if (currentFen) { const currentHash = hashFen(currentFen); if (currentHash !== fenHash && normalizeFen(currentFen) !== normalizeFen(fen)) { this.executedFens.delete(fenHash); return false; } }
const from = uci.slice(0, 2), to = uci.slice(2, 4), promotion = uci.length > 4 ? uci.slice(4) : null;
const ourColor = getPlayingAs();
if (!ourColor) { this.executedFens.delete(fenHash); return false; }
const preExecCheck = this._validatePreExecution(fenHash, uci, ourColor, true);
if (!preExecCheck.ok) { this.executedFens.delete(fenHash); return false; }
const ok = await MoveExecutor._clickMove(from, to, promotion, true);
if (ok === true) {
this.consecutiveErrors = 0;
this.moveHistory.push({ move: uci, timeSpent: delay, wasEngineMove: true, timestamp: Date.now(), confidence: decision.confidence, mode: decision.mode || State.premoveMode });
if (this.moveHistory.length > this.MAX_HISTORY) this.moveHistory = this.moveHistory.slice(-this.MAX_HISTORY);
this.lastSafeMoves.push({ fen, uci, timestamp: Date.now() });
if (this.lastSafeMoves.length > 10) this.lastSafeMoves.shift();
} else { this.consecutiveErrors++; }
return ok;
} catch (e) { console.error("SmartPremove execution error:", e); this.consecutiveErrors++; return false; }
finally { this.executionLock = false; this.processingLock = false; this.lastExecutionTime = Date.now(); }
},
_humanDelay(base) { const u1 = Math.random(), u2 = Math.random(); const normal = Math.sqrt(-2 * Math.log(Math.max(u1, 0.001))) * Math.cos(2 * Math.PI * u2); const delay = base + normal * (base * 0.25); const thinkPause = Math.random() < 0.1 ? (base * 0.5 + Math.random() * base) : 0; return Math.min(this.HEURISTICS.DELAY_MAX_MS, Math.max(this.HEURISTICS.DELAY_MIN_MS, delay + thinkPause)); },
getStats() { const pattern = this.detectPattern(); return { executedPositions: this.executedFens.size, moveHistory: this.moveHistory.length, consecutiveErrors: this.consecutiveErrors, patternBreaks: this.patternBreakCounter, blunderCount: this.blunderCount, isLocked: this.executionLock || this.processingLock, inCooldown: this.isInErrorCooldown(), inCautiousMode: this.isInCautiousMode(), pattern, lastExecutionTime: this.lastExecutionTime, recentMoves: this.moveHistory.slice(-5) }; }
};
// =====================================================
// Section 10: Engine Execution Functions
// =====================================================
let _pendingEngineRunId = null;
function runEngineNow() {
let fen = getAccurateFen();
if (!fen) { warn("Cannot get FEN"); return; }
if (_pendingEngineRunId) { clearTimeout(_pendingEngineRunId); _pendingEngineRunId = null; }
if (State.isThinking) { Engine.stop(); _pendingEngineRunId = scheduleManagedTimeout(function () { _doRun(fen); }, 100); }
else { _doRun(fen); }
}
let _goLock = false;
function _doRun(fen) {
if (_goLock) return;
_goLock = true;
try {
if (State.useOpeningBook && State.evaluationMode === "engine") {
let history = getGameHistory();
let bookMove = OpeningBook.getMove(fen, history);
if (bookMove) { State.isThinking = false; State.statusInfo = "Book Move: " + bookMove; UI.updateStatusInfo(); MoveExecutor.recordMove(bookMove); if (State.autoMovePiece) executeAction(bookMove, fen); return; }
}
Engine.go(fen, State.customDepth);
} finally {
_goLock = false;
}
}
function autoRunCheck() {
if (!State.autoRun || State.isThinking || !isPlayersTurn()) return;
let fen = getAccurateFen();
if (!fen || fen === State.lastAutoRunFen) return;
State.lastAutoRunFen = fen;
runEngineNow();
}
function syncAnalysisMoveHistory() {
if (!State.analysisMode) return;
let history = getGameHistory();
if (!Array.isArray(history)) return;
State.analysisHistoryCursor = history.length;
}
function recordAnalysisBestmove(move, evalText, depth, fen, sourceTag) {
if (!State.analysisMode || !move || move.length < 4) return;
let fenKey = hashFen(fen || getAccurateFen() || "");
let recordKey = fenKey + "|" + move;
if (State.analysisLastRecordedKey === recordKey) return;
MoveHistory.add(move, evalText || State.analysisEvalText || State.lastEvalText1 || "0.00", depth || State._lastAnalysisDepth || State.customDepth, "Analysis", null, sourceTag || "Analysis");
State.analysisLastRecordedKey = recordKey;
}
function analysisCheck() {
if (!State.analysisMode || !Engine.analysis) return;
let fen = getAccurateFen();
if (!fen) return;
if (fen === State._lastAnalysisFen) return;
syncAnalysisMoveHistory();
UI.clearAll();
State.topMoves = []; State.topMoveInfos = {};
let maxRows = clamp(State.numberOfMovesToShow || 5, 2, 10);
for (let i = 1; i <= maxRows; i++) { UI.updateMove(i, "...", "0.00", "eval-equal"); }
State._lastAnalysisFen = fen; State.analysisPVTurn = getCurrentTurn(fen); State.isAnalysisThinking = true;
State._lastAnalysisDepth = 0; State._analysisDepthByPv = {}; State._lastAnalysisBestPV = []; State._lastAnalysisBestMove = null;
State.analysisPVLine = []; State.analysisStableCount = 0; State.analysisLastBestMove = "";
State.analysisPrevEvalCp = null; State.analysisLastEvalCp = null;
State._analysisAutoPlayApproved = false; State._analysisAutoPlayMove = null;
Syzygy.maybeProbe(fen);
if (Syzygy.tryUseForAnalysis(fen)) return;
Engine.analysis.postMessage("stop");
Engine.analysis.postMessage("position fen " + fen);
Engine.analysis.postMessage("go depth " + State.customDepth);
State.statusInfo = "Analyzing..."; UI.updateStatusInfo();
}
// =====================================================
// Section 11: Premove Check
// =====================================================
function premoveCheck() {
if (!State.premoveEnabled) return;
let now = Date.now();
if (now - State.premoveLastAnalysisTime < State.premoveThrottleMs) return;
if (Engine._premoveEngineBusy) return;
const fen = getAccurateFen();
if (!fen) return;
const fenHash = hashFen(fen);
if (State.premoveExecutedForFen === fenHash) return;
if (Engine._premoveProcessedFens.has(fenHash)) return;
const game = getGame();
if (!game || isPlayersTurn(game)) { State.premoveExecutedForFen = null; return; }
if (State.premoveAnalysisInProgress) return;
if (Engine._premoveLastFen === fenHash) return;
State.premoveAnalysisInProgress = true;
State.premoveLastAnalysisTime = now;
if (!Engine.premove) {
let loaded = Engine.loadPremoveEngine();
if (!loaded) { State.premoveAnalysisInProgress = false; return; }
scheduleManagedTimeout(function () { _startPremoveAnalysis(fen, fenHash); }, 200);
} else {
_startPremoveAnalysis(fen, fenHash);
}
UI.updateStatusInfo();
}
function _startPremoveAnalysis(fen, fenHash) {
Engine._premoveEngineBusy = true; Engine._premoveLastFen = fenHash; Engine._premoveLastActivityTs = Date.now();
Engine.premove.postMessage("stop"); Engine.premove.postMessage("ucinewgame");
scheduleManagedTimeout(function () {
const freshFen = getAccurateFen();
if (hashFen(freshFen) !== fenHash) { Engine._premoveEngineBusy = false; State.premoveAnalysisInProgress = false; return; }
Engine.premove.postMessage("position fen " + fen);
Engine.premove.postMessage("go depth " + (State.premoveDepth || 15));
Engine._premoveLastActivityTs = Date.now();
if (Engine._premoveTimeoutId) { clearTimeout(Engine._premoveTimeoutId); Engine._premoveTimeoutId = null; }
Engine._premoveTimeoutId = setTimeout(function () { Engine._premoveEngineBusy = false; State.premoveAnalysisInProgress = false; Engine._premoveLastActivityTs = Date.now(); if (Engine.premove) Engine.premove.postMessage("stop"); }, CONFIG.PREMOVE.ENGINE_TIMEOUT);
}, 50);
State.statusInfo = "Smart premove analyzing..."; UI.updateStatusInfo();
}
function autoMatchCheck() {
if (!State.autoMatch) return;
if (State.analysisMode) return;
let modal = $(".game-result-component, .game-over-modal-shell-content, .daily-game-footer-game-over");
if (!modal) return;
AutoMatch.try();
}
// =====================================================
// Section 12: User Interface Panel Construction
// =====================================================
function getPanelHTML() {
let mode = State.evaluationMode;
let modeText = mode === "engine" ? "ENGINE" : "HUMAN";
let modeClass = mode === "engine" ? "on" : "off";
let multiPvCount = 2;
let safeNotationSequence = escapeHtml(String(State.notationSequence || ""));
let safePrincipalVariation = escapeHtml(String(State.principalVariation || "Waiting for analysis..."));
let safeStatusInfo = escapeHtml(String(State.statusInfo || "Ready"));
let topMovesRows = "";
for (let i = 1; i <= multiPvCount; i++) {
topMovesRows += '
' + i + '. ... 0.00
';
}
let eloOptions = "";
let keys = Object.keys(ELO_LEVELS);
for (let i = 0; i < keys.length; i++) {
let k = keys[i];
let v = ELO_LEVELS[k];
let sel = State.humanLevel === k ? " selected" : "";
eloOptions += "" + k.charAt(0).toUpperCase() + k.slice(1) + " (" + v.elo + ") ";
}
let resignMateOptions = "";
for (let m = 1; m <= 5; m++) {
let s = State.autoResignThresholdMate === m ? " selected" : "";
resignMateOptions += "M" + m + " ";
}
return '' +
'
' +
'' +
'' +
'
' +
'
' +
'
Engine
' +
'
Premove
' +
'
Time
' +
'
Display
' +
'
Book
' +
'
Moves
' +
'
More
' +
'
' +
'
' +
'
' +
'
Engine Mode ' +
'' + modeText + '
' +
'
' +
'Human Level ' + eloOptions + '
' +
'
' +
'ELO: ' + State.eloRating + ' ' +
'
' +
'
' +
'
' +
'
Auto Depth by Opponent ' +
'' + (State.autoDepthAdapt ? "ON" : "OFF") + '
' +
'
Move Execution Mode ' +
'CLICK ' +
'DRAG ' +
'
' +
'
' +
'
' +
'
Auto Run ' + (State.autoRun ? "ON" : "OFF") + '
' +
'
Auto Move ' + (State.autoMovePiece ? "ON" : "OFF") + '
' +
'
Auto Play ' + (State.autoMatch ? "ON" : "OFF") + '
' +
'
' +
'
Analysis Mode ' + (State.analysisMode ? "ON" : "OFF") + '
' +
'
' +
'
Auto Play Color ' +
'White ' +
'Black ' +
'Off ' +
'
' +
'
' +
'
' +
'
Premove System ' +
'' + (State.premoveEnabled ? "ON" : "OFF") + '
' +
'
' +
'
Premove Mode ' +
'Every Move ' +
'Captures Only ' +
'Filtered Pieces
' +
'
' +
'
Premove Stats A:0 OK:0 EX:0 BL:0 FL:0
' +
'
' +
'
CCT Analysis (Checks, Captures, Threats) ' +
'' + (State.cctAnalysisEnabled ? "ON" : "OFF") + '
' +
'
' +
'
How it works:
' +
'Analyzes opponent likely move Pre-calculates your best response ' +
'Executes instantly when opponent moves CCT safety checks prevent blunders ' +
'
' +
'
' +
'
' +
'
Delay Mode ' +
'' + (State.clockSync ? "Clock Sync" : "Normal") + '
' +
'
Presets ' +
'Bullet ' +
'Blitz ' +
'Rapid ' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'Normal delay mengikuti pengaturan Min/Max Delay di atas.' +
'
' +
'
' +
'
' +
'
' +
'💡 Jika waktu < ' + State.clockSyncLowTimeQuickSec + ' s, delay otomatis = ' + (State.clockSyncQuickDelayMs || 300) + ' ms' +
'
' +
'
Auto Resign ' +
'' + (State.autoResignEnabled ? "ON" : "OFF") + '
' +
'
' +
'
Mode ' +
'Mate in ' +
'Centipawn
' +
'
' +
'Mate in ' + resignMateOptions + '
' +
'
' +
'CP threshold
' +
'
' +
'
' +
'
' +
'
PV Arrows ' + (State.showPVArrows ? "ON" : "OFF") + '
' +
'
Bestmove ' + (State.showBestmoveArrows ? "ON" : "OFF") + '
' +
'
Highlights ' + (State.highlightEnabled ? "ON" : "OFF") + '
' +
'
' +
'
PV Depth: ' + State.maxPVDepth + ' moves ' +
'
' +
'
Arrow Color (Auto) ' +
'
' +
'
' +
' ' +
' ' +
' ' +
' ' +
'
' +
'
Arrow Color (PV) ' +
'
' +
'
' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
'
' +
'
Arrow Color (Bestmove) ' +
'
' +
'
' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
'
' +
'
' +
'
' +
'
' +
'
Use Opening Book ' +
'' + (State.useOpeningBook ? "ON" : "OFF") + '
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
topMovesRows +
'
' +
'
' +
'
' +
'
Search Move History
' +
'
# Move Eval D Grade Source Time ' +
'
' +
'
' +
'
' +
'
Reload
' +
'
Run
' +
'
Stop
' +
'
' +
'
Principal Variation
' +
'
' + safePrincipalVariation + '
' +
'
Status
' +
'
Current Status ' +
'' + safeStatusInfo + '
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'' +
'
' +
'
' +
'
Soft Analysis Reset
' +
'
Soft Premove Reset
' +
'
' +
'
' +
'
Export Settings
' +
'
Import Settings
' +
'
' +
'
Notation Sequence (UCI) ' +
'
' +
'
Diagnostics
' +
'
Workers M:- A:- P:-
' +
'
Caches PR:0 CCT:0 TH:0
' +
'
Runtime T:0 L:0
' +
'
Errors En:0 UI:0 Pr:0 Sy:0 Rt:0 O:0
' +
'
Self Test R:0 F:0 UIH:0 LH:0
' +
'
Smart Controls
' +
'
' +
'
Main Consensus Move ' +
'' + (State.useMainConsensus ? "ON" : "OFF") + '
' +
'
Analysis Blunder Guard ' +
'' + (State.analysisBlunderGuard ? "ON" : "OFF") + '
' +
'
' +
'
Silent Logs ' + (State.silentLogging ? "ON" : "OFF") + '
' +
'
Stable Updates Required ' +
'
' +
'
Analysis Stability ' + State.analysisStableCount + 'x
' +
'
Guard Status Ready
' +
'
' +
'
' +
'
' +
'
' +
'';
}
// =====================================================
// Section 13: Panel CSS Styling
// =====================================================
function getPanelCSS() {
return "#chess-assist-panel{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:" + CONFIG.PANEL_WIDTH + "px;background:#1e1e2e;border:1px solid #444;border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.6);z-index:99999;font-family:'Segoe UI',-apple-system,BlinkMacSystemFont,Roboto,sans-serif;color:#cdd6f4;font-size:12px;overflow:hidden;user-select:none}" +
"#chess-assist-panel.minimized .cap-tabs,#chess-assist-panel.minimized .cap-content,#chess-assist-panel.minimized .cap-eval-footer{display:none!important}" +
"#chess-assist-panel.closed{display:none!important}" +
".cap-panel{display:flex;flex-direction:column}" +
".cap-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:#313244;cursor:grab;border-bottom:1px solid #45475a}" +
".cap-header:active{cursor:grabbing}" +
".cap-header-left{display:flex;align-items:center;gap:8px}" +
".cap-title{font-weight:700;font-size:13px;color:#a6e3a1;letter-spacing:0.5px}" +
".cap-leds{display:flex;gap:4px}" +
".cap-led{width:8px;height:8px;border-radius:50%;background:#45475a;transition:all .3s}" +
".cap-led.green.active{background:#a6e3a1;box-shadow:0 0 6px #a6e3a1}" +
".cap-led.blue.active{background:#89b4fa;box-shadow:0 0 6px #89b4fa}" +
".cap-led.red.active{background:#f38ba8;box-shadow:0 0 6px #f38ba8}" +
".cap-clock{font-family:'Courier New',monospace;font-size:11px;color:#6c7086}" +
".cap-header-btns{display:flex;gap:4px}" +
".cap-header-btns button{background:#45475a;border:none;color:#cdd6f4;width:22px;height:22px;border-radius:4px;cursor:pointer;font-size:13px;line-height:1;transition:background .2s}" +
".cap-header-btns #cap-gear{font-size:14px}" +
".cap-gear-wrap{position:relative}" +
".cap-gear-menu{position:absolute;top:28px;right:0;min-width:150px;background:#181825;border:1px solid #45475a;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.45);padding:6px;z-index:100120;display:flex;flex-direction:column;gap:4px}" +
".cap-gear-menu button{width:100%;height:auto;padding:7px 8px;border-radius:6px;text-align:left;font-size:11px;background:#313244;color:#cdd6f4}" +
".cap-gear-menu button:hover{background:#45475a}" +
".cap-overlay{position:fixed;inset:0;z-index:100260;display:flex;align-items:center;justify-content:center}" +
".cap-overlay-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.62)}" +
".cap-overlay-modal{position:relative;width:min(620px,95vw);max-height:90vh;overflow:auto;background:#1e1e2e;border:1px solid #45475a;border-radius:10px;padding:12px;box-shadow:0 14px 44px rgba(0,0,0,.55)}" +
".cap-overlay-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}" +
".cap-overlay-header button{width:22px;height:22px;border:none;border-radius:4px;background:#45475a;color:#cdd6f4;cursor:pointer}" +
".cap-overlay-body{padding-top:4px}" +
".cap-header-btns button:hover{background:#585b70}" +
".cap-tabs{display:flex;align-items:center;background:#181825;border-bottom:1px solid #45475a;overflow-x:auto;min-height:30px}" +
".cap-tab{flex:1;padding:6px 4px;text-align:center;font-size:9px;cursor:pointer;white-space:nowrap;background:#1e1e2e;transition:all .2s;border-right:1px solid #313244}" +
".cap-tab:last-child{border-right:none}" +
".cap-tab:hover{background:#313244}" +
".cap-tab.active{background:#a6e3a1;color:#1e1e2e;font-weight:700}" +
".cap-content{overflow-y:auto;height:360px;padding:12px;scrollbar-width:thin;scrollbar-color:#45475a #1e1e2e}" +
".cap-tab-content{animation:capFadeIn .2s ease}" +
"@keyframes capFadeIn{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}" +
".cap-group{margin-bottom:12px;padding:10px;background:#313244;border-radius:8px}" +
".cap-group label{display:block;margin-bottom:6px;font-size:11px;color:#a6adc8}" +
".cap-row{display:flex;gap:8px}" +
".cap-half{flex:1}" +
".cap-third{flex:1}" +
".cap-toggle{width:100%;padding:7px 12px;border:none;border-radius:6px;cursor:pointer;font-weight:600;font-size:12px;transition:all .2s}" +
".cap-toggle.on{background:#a6e3a1;color:#1e1e2e}" +
".cap-toggle.off{background:#45475a;color:#6c7086}" +
".cap-toggle:hover{filter:brightness(1.1)}" +
"select,input[type=\"number\"]{width:100%;padding:6px 8px;background:#45475a;border:1px solid #585b70;color:#cdd6f4;border-radius:6px;font-size:12px}" +
"select:focus,input:focus{outline:none;border-color:#a6e3a1}" +
"input[type='range']{width:100%;-webkit-appearance:none;height:6px;background:linear-gradient(90deg,#3b3a38,#464442);border-radius:6px;outline:none}" +
"input[type='range']::-webkit-slider-thumb{-webkit-appearance:none;width:22px;height:26px;background:linear-gradient(180deg,#d6d6d6,#9a9a9a);border-radius:4px;cursor:pointer;margin-top:-10px;box-shadow:inset 0 2px 2px rgba(255,255,255,.4),inset 0 -2px 2px rgba(0,0,0,.4),0 2px 6px rgba(0,0,0,.6);transition:all .2s;position:relative}" +
"input[type='range']::-webkit-slider-thumb:before{content:'';position:absolute;left:50%;top:4px;transform:translateX(-50%);width:3px;height:18px;background:#333;border-radius:2px}" +
"input[type='range']::-webkit-slider-thumb:hover{background:linear-gradient(180deg,#ffffff,#bcbcbc);transform:scale(1.05)}" +
"input[type='range']::-moz-range-thumb{width:22px;height:26px;background:linear-gradient(180deg,#d6d6d6,#9a9a9a);border-radius:4px;border:none;cursor:pointer}" +
"#sld-depth{-webkit-appearance:none;width:100%;height:7px;background:linear-gradient(90deg,#3b3a38,#464442);border-radius:6px;outline:none}" +
"#sld-depth::-webkit-slider-thumb{-webkit-appearance:none;width:22px;height:26px;background:linear-gradient(180deg,#89b4fa,#5fa2ff);border-radius:4px;cursor:pointer;margin-top:-10px;box-shadow:inset 0 2px 2px rgba(255,255,255,.4),inset 0 -2px 2px rgba(0,0,0,.4),0 2px 6px rgba(0,0,0,.6);transition:all .2s;position:relative}" +
"#sld-depth::-webkit-slider-thumb:before{content:'';position:absolute;left:50%;top:4px;transform:translateX(-50%);width:3px;height:18px;background:#333;border-radius:2px}" +
"#sld-depth::-webkit-slider-thumb:hover{background:linear-gradient(180deg,#a6e3a1,#7edb87);transform:scale(1.05)}" +
"#sld-depth::-moz-range-thumb{width:22px;height:26px;background:#89b4fa;border-radius:4px;border:none;cursor:pointer}" +
"#sld-skill{-webkit-appearance:none;width:100%;height:7px;background:linear-gradient(90deg,#3b3a38,#464442);border-radius:6px;outline:none}" +
"#sld-skill::-webkit-slider-thumb{-webkit-appearance:none;width:22px;height:26px;background:linear-gradient(180deg,#f9e2af,#f5c211);border-radius:4px;cursor:pointer;margin-top:-10px;box-shadow:inset 0 2px 2px rgba(255,255,255,.4),inset 0 -2px 2px rgba(0,0,0,.4),0 2px 6px rgba(0,0,0,.6);transition:all .2s;position:relative}" +
"#sld-skill::-webkit-slider-thumb:hover{background:linear-gradient(180deg,#ffe066,#ffd700);transform:scale(1.05)}" +
"#sld-skill::-moz-range-thumb{width:22px;height:26px;background:#f9e2af;border-radius:4px;border:none;cursor:pointer}" +
".mixer-btn{width:40px;height:40px;background:linear-gradient(180deg,#d6d6d6,#9a9a9a);border-radius:6px;box-shadow:inset 0 2px 2px rgba(255,255,255,.4),inset 0 -2px 2px rgba(0,0,0,.4),0 2px 4px rgba(0,0,0,.5);cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;justify-content:center;margin:4px}" +
".mixer-btn:hover{background:linear-gradient(180deg,#ffffff,#bcbcbc);transform:scale(1.05)}" +
".mixer-btn i{font-size:18px;color:#333}" +
"input[type=\"color\"]{width:50px;height:30px;border:none;border-radius:6px;cursor:pointer;vertical-align:middle}" +
".cap-color-row{display:flex;align-items:center;gap:8px}" +
".cap-presets{display:flex;gap:4px}" +
".cap-preset{width:22px;height:22px;border-radius:50%;border:2px solid transparent;cursor:pointer;transition:border-color .2s}" +
".cap-preset:hover{border-color:#fff}" +
".cap-pv-presets .cap-preset.active{border-color:#89b4fa;box-shadow:0 0 0 1px #89b4fa inset}" +
".cap-bm-presets .cap-preset.active{border-color:#a6e3a1;box-shadow:0 0 0 1px #a6e3a1 inset}" +
".cap-btn-row{display:flex;gap:6px}" +
".cap-color-btn{flex:1;padding:6px;border:none;border-radius:6px;background:#45475a;color:#a6adc8;cursor:pointer;font-size:11px;transition:all .2s}" +
".cap-color-btn.active{background:#a6e3a1;color:#1e1e2e;font-weight:600}" +
".cap-color-btn:hover{filter:brightness(1.1)}" +
".cap-preset-btn{flex:1;padding:8px 6px;border:2px solid #45475a;border-radius:6px;background:#313244;color:#a6adc8;cursor:pointer;font-size:11px;font-weight:600;transition:all .2s;white-space:nowrap}" +
".cap-preset-btn:hover{border-color:#a6e3a1;background:#45475a;filter:brightness(1.1)}" +
".cap-preset-btn.active{background:#a6e3a1;color:#1e1e2e;border-color:#a6e3a1;box-shadow:0 0 8px rgba(166,227,161,0.4)}" +
".cap-action-btn{width:100%;padding:10px;border:none;border-radius:6px;background:#45475a;color:#cdd6f4;font-weight:600;cursor:pointer;font-size:12px;transition:all .2s}" +
".cap-action-btn:hover{filter:brightness(1.2)}" +
".cap-action-btn.green{background:#a6e3a1;color:#1e1e2e}" +
".cap-action-btn.red{background:#f38ba8;color:#1e1e2e}" +
".setting-group{margin-bottom:12px;padding:10px;background:#313244;border-radius:8px}" +
".setting-group h5{margin:0 0 8px 0;font-size:12px;color:#a6adc8}" +
".pv-display{padding:8px;background:#181825;border:1px solid #45475a;border-radius:6px;font-family:monospace;font-size:10px;color:#cdd6f4;word-break:break-all}" +
".setting-row{display:flex;justify-content:space-between;align-items:center}" +
".setting-label{font-size:11px;color:#a6adc8}" +
".setting-value{font-weight:700;font-size:12px;color:#cdd6f4}" +
".divider{height:1px;background:#45475a;margin:8px 0}" +
".cap-opening-box{text-align:center;padding:20px;background:#313244;border-radius:8px;margin-bottom:12px}" +
".cap-opening-label{font-size:11px;color:#6c7086;margin-bottom:5px}" +
".cap-opening-name{font-size:18px;font-weight:700;color:#89b4fa}" +
".cap-book-row{align-items:flex-end;gap:8px;margin-bottom:8px}" +
".cap-book-row .cap-group{margin-bottom:0}" +
".cap-delay-head-row{align-items:stretch;gap:8px;margin-bottom:8px}" +
".cap-delay-head-row .cap-group{margin-bottom:0}" +
".cap-delay-head-group .cap-toggle{height:34px;padding:0 12px}" +
"#delay-presets-container{height:34px}" +
"#delay-presets-container .cap-preset-btn{padding:6px 4px}" +
".cap-opening-inline{height:34px;display:flex;align-items:center;padding:0 10px;background:#45475a;border:1px solid #585b70;border-radius:6px}" +
".cap-opening-name.inline{font-size:13px;font-weight:700;color:#89b4fa;line-height:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}" +
".cap-book-syzygy{margin-top:0}" +
".cap-book-hint{font-size:10px;color:#6c7086}" +
".cap-book-syzygy-body{padding:8px;background:#313244;border-radius:6px}" +
".cap-book-syzygy-status{font-size:10px;color:#a6adc8;margin-bottom:6px}" +
".cap-book-syzygy-scroll{max-height:120px}" +
".cap-top-moves{margin-bottom:12px}" +
".cap-move-row{display:flex;align-items:center;padding:8px 10px;background:#313244;border-radius:6px;margin-bottom:4px}" +
".cap-rank{width:30px;color:#a6e3a1;font-weight:700;font-size:11px}" +
".cap-move-text{flex:1;font-weight:700;font-family:monospace;font-size:13px}" +
".eval{text-align:right;font-weight:600;font-size:12px}" +
".eval-positive{color:#a6e3a1}" +
".eval-negative{color:#f38ba8}" +
".eval-equal{color:#f9e2af}" +
".eval-mate{color:#cba6f7}" +
".cap-acpl{margin-bottom:12px}" +
".cap-acpl-header{display:flex;justify-content:space-between;font-size:11px;margin-bottom:6px;color:#a6adc8}" +
".cap-acpl-bars{display:flex;flex-direction:column;gap:4px}" +
".cap-acpl-bar-row{display:flex;align-items:center;gap:6px}" +
".cap-acpl-label{width:16px;font-size:10px;font-weight:700}" +
".cap-acpl-bar-bg{flex:1;height:14px;background:#45475a;border-radius:3px;overflow:hidden}" +
".cap-acpl-bar{height:100%;width:0%;transition:width .4s;border-radius:3px}" +
".cap-acpl-bar.white{background:#a6e3a1}" +
".cap-acpl-bar.black{background:#f38ba8}" +
".cap-history-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}" +
".cap-clear-btn{padding:4px 10px;background:#f38ba8;color:#1e1e2e;border:none;border-radius:4px;cursor:pointer;font-size:10px;font-weight:600}" +
".cap-history-scroll{max-height:140px;overflow-y:auto;background:#313244;border-radius:6px}" +
"#inp-move-filter{width:100%;padding:6px 8px;background:#45475a;border:1px solid #585b70;color:#cdd6f4;border-radius:6px;font-size:11px}" +
"#inp-move-filter:focus{outline:none;border-color:#a6e3a1}" +
"#moveHistoryTable{width:100%;border-collapse:collapse;font-size:10px}" +
"#moveHistoryTable th{background:#45475a;padding:5px 4px;text-align:center;position:sticky;top:0;z-index:1;color:#a6adc8}" +
"#moveHistoryTable td{padding:4px;text-align:center;border-bottom:1px solid #45475a}" +
"#syzygyTable{width:100%;border-collapse:collapse;font-size:10px;background:#2a2b3a}" +
"#syzygyTable th{background:#45475a;padding:5px 6px;text-align:left;position:sticky;top:0;z-index:1;color:#a6adc8;font-weight:700;border-bottom:1px solid #585b70}" +
"#syzygyTable td{padding:5px 6px;text-align:left;border-bottom:1px solid #45475a;color:#cdd6f4;font-family:monospace}" +
"#syzygyTable tbody tr:nth-child(even){background:rgba(69,71,90,0.22)}" +
"#syzygyTable tbody tr:last-child td{border-bottom:none}" +
"#syzygyTable td:first-child,#syzygyTable th:first-child{width:28px;text-align:center;font-family:'Segoe UI',sans-serif}" +
"#syzygyTable td:nth-child(2){font-weight:700;letter-spacing:0.2px}" +
".cap-eval-footer{padding:8px 12px;background:#313244;border-top:1px solid #45475a}" +
".cap-eval-bar-wrap{position:relative;height:22px;background:#45475a;border-radius:5px;overflow:hidden;margin-bottom:4px}" +
".cap-eval-bar-wrap.small{height:8px;margin-bottom:0}" +
".cap-eval-fill{height:100%;width:50%;background:#a6e3a1;transition:all .4s;border-radius:5px}" +
".cap-eval-fill.analysis{background:#89b4fa}" +
".cap-eval-label{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:10px;font-weight:600;text-shadow:0 0 3px #000;white-space:nowrap}" +
".cap-info-box{padding:10px;background:#313244;border-radius:6px;font-size:11px;line-height:1.6;margin-bottom:8px}" +
".cap-info-box ul{margin:4px 0 0 16px;padding:0}" +
".piece-filters{display:flex;gap:6px;flex-wrap:wrap}" +
".chip{display:flex;align-items:center;gap:4px;padding:4px 8px;background:#45475a;border-radius:4px;cursor:pointer;font-size:11px}" +
".chip input{margin:0}" +
".cap-content::-webkit-scrollbar,.cap-history-scroll::-webkit-scrollbar{width:5px}" +
".cap-content::-webkit-scrollbar-track,.cap-history-scroll::-webkit-scrollbar-track{background:transparent}" +
".cap-content::-webkit-scrollbar-thumb,.cap-history-scroll::-webkit-scrollbar-thumb{background:#45475a;border-radius:3px}" +
".chess-assist-arrow{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999}" +
".chess-assist-arrow[data-analysis=\"true\"]{z-index:10001!important}" +
".chess-assist-pv-arrow{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9990}" +
".chess-assist-pv-arrow[data-analysis=\"true\"]{z-index:10090!important}" +
".chess-assist-bestmove-arrow{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}" +
".chess-assist-arrow,.chess-assist-pv-arrow,.chess-assist-bestmove-arrow{transition:opacity 0.2s ease}" +
".chess-assist-arrow rect{filter:drop-shadow(0 0 4px currentColor)}" +
".chess-assist-arrow[data-analysis=\"true\"] rect{filter:drop-shadow(0 0 6px currentColor)}" +
"#premoveChanceDisplay.high-chance{color:#f38ba8;font-weight:bold;animation:pulse 1s infinite}" +
"#premoveChanceDisplay.low-chance{color:#a6e3a1}" +
"@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}" +
".chess-assist-pv-arrow line{stroke-linecap:round}" +
".chess-assist-pv-arrow circle{opacity:0.9}" +
".chess-assist-pv-arrow text{font-family:'Segoe UI',sans-serif}" +
".cap-welcome-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.72);z-index:100200;display:flex;align-items:center;justify-content:center;padding:16px}" +
".cap-welcome-modal{width:min(560px,94vw);background:#1e1e2e;border:1px solid #45475a;border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,0.6);padding:16px;color:#cdd6f4}" +
".cap-welcome-title{font-size:18px;font-weight:700;color:#a6e3a1;margin-bottom:8px}" +
".cap-welcome-subtitle{font-size:12px;color:#bac2de;line-height:1.5;margin-bottom:10px}" +
".cap-welcome-list{margin:0 0 10px 18px;padding:0;font-size:12px;line-height:1.6;color:#cdd6f4}" +
".cap-welcome-warning{background:#2a1d22;border:1px solid #5b3240;border-radius:8px;padding:10px;font-size:12px;line-height:1.5;color:#f2cdcd;margin-bottom:12px;text-align:center}" +
".cap-welcome-warning-line{height:2px;background:#f38ba8;border-radius:2px;margin:0 0 8px 0}" +
".cap-welcome-warning-title{display:block;font-weight:700;color:#f38ba8;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.4px}" +
".cap-welcome-warning-body{display:block;color:#f2cdcd;line-height:1.55}" +
".cap-welcome-consent{display:flex;align-items:flex-start;gap:8px;font-size:12px;color:#cdd6f4;margin-bottom:12px}" +
".cap-welcome-consent input{margin-top:2px}" +
".cap-welcome-actions{display:flex;gap:8px;justify-content:flex-end}" +
".cap-welcome-btn{padding:8px 12px;border:none;border-radius:6px;cursor:pointer;font-weight:600;font-size:12px}" +
".cap-welcome-btn.primary{background:#a6e3a1;color:#1e1e2e}" +
".cap-welcome-btn.primary:disabled{background:#6c7086;color:#313244;cursor:not-allowed}" +
".cap-welcome-btn.secondary{background:#45475a;color:#cdd6f4}" +
".cap-hotkeys-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.72);z-index:100250;display:flex;align-items:center;justify-content:center;padding:16px}" +
".cap-hotkeys-modal{width:min(520px,94vw);background:#1e1e2e;border:1px solid #45475a;border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,0.6);padding:14px;color:#cdd6f4}" +
".cap-hotkeys-title{font-size:16px;font-weight:700;color:#a6e3a1;margin-bottom:8px}" +
".cap-hotkeys-table{width:100%;border-collapse:collapse;font-size:12px;background:#181825;border-radius:8px;overflow:hidden}" +
".cap-hotkeys-table th,.cap-hotkeys-table td{padding:8px 10px;border-bottom:1px solid #313244;text-align:left}" +
".cap-hotkeys-table th{color:#a6adc8;font-size:11px;background:#11111b}" +
".cap-hotkeys-actions{display:flex;justify-content:flex-end;margin-top:10px}" +
"@media (max-width: 768px) {" +
"#chess-assist-panel{width:90vw!important;height:auto;max-height:90vh;top:50%!important;left:50%!important;transform:translate(-50%,-50%)!important;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,0.8)}" +
".cap-content{height:300px!important;overflow-y:auto}" +
".cap-header{padding:6px 10px}" +
".cap-title{font-size:12px}" +
".cap-tabs{font-size:9px}" +
".cap-group{margin-bottom:8px;padding:8px}" +
".cap-preset-btn{font-size:10px;padding:6px 4px}" +
"select,input[type=\"number\"]{font-size:11px;padding:5px 6px}" +
"input[type=\"range\"]::-webkit-slider-thumb{width:12px;height:12px}" +
"}" +
"@media (max-width: 480px) {" +
"#chess-assist-panel{width:95vw!important;max-height:88vh}" +
".cap-content{height:250px!important;font-size:11px}" +
".cap-header{padding:5px 8px}" +
".cap-title{font-size:11px}" +
".cap-tabs{font-size:8px;overflow-x:auto}" +
".cap-tab{padding:5px 2px}" +
".cap-group{margin-bottom:6px;padding:6px;font-size:10px}" +
".cap-group label{font-size:10px;margin-bottom:4px}" +
".cap-toggle{padding:5px 8px;font-size:10px}" +
".cap-delay-head-row{gap:6px}" +
".cap-preset-btn{font-size:9px;padding:4px 2px}" +
".cap-header-btns button{width:20px;height:20px;font-size:12px}" +
"select,input[type=\"number\"],input[type=\"text\"]{font-size:10px;padding:4px 5px}" +
".cap-row{gap:4px}" +
".cap-move-text{font-size:11px}" +
".cap-move-row{padding:6px 8px}" +
".eval{font-size:10px}" +
"#moveHistoryTable{font-size:9px}" +
"#moveHistoryTable th,#moveHistoryTable td{padding:2px 2px}" +
"#syzygyTable{font-size:9px}" +
"#syzygyTable th,#syzygyTable td{padding:3px 4px}" +
".cap-history-scroll{max-height:120px}" +
".cap-eval-footer{padding:6px 8px}" +
".cap-eval-bar-wrap{height:18px}" +
".cap-preset{width:18px;height:18px}" +
"input[type=\"color\"]{width:40px;height:25px}" +
".cap-color-btn{padding:4px;font-size:9px}" +
".piece-filters{gap:4px}" +
".chip{padding:2px 4px;font-size:9px}" +
".cap-opening-name{font-size:14px}" +
".cap-acpl-bar-row{gap:4px}" +
".cap-acpl-bar-bg{height:10px}" +
"}" +
"@media (max-width: 380px) {" +
"#chess-assist-panel{width:98vw!important}" +
".cap-content{height:200px!important}" +
".cap-header-btns button{width:18px;height:18px;font-size:11px}" +
".cap-title{font-size:10px}" +
".cap-group{padding:4px}" +
"select,input{font-size:9px}" +
".cap-toggle{font-size:9px;padding:4px 6px}" +
"}" +
"@media (orientation: landscape) and (max-height: 500px) {" +
"#chess-assist-panel{height:90vh!important;max-height:90vh!important}" +
".cap-content{height:60vh!important;max-height:60vh!important}" +
".cap-history-scroll{max-height:100px}" +
"}";
}
// =====================================================
// Section 14: Panel DOM Creation and Insertion
// =====================================================
function runHealthCheck() {
const report = getDiagnosticsSnapshot();
const runtime = report.runtime;
const caches = report.caches;
const workers = report.workers;
log("[HealthCheck]", report);
State.statusInfo = "Health check OK | Workers M=" + (workers.main ? 1 : 0) +
" A=" + (workers.analysis ? 1 : 0) +
" P=" + (workers.premove ? 1 : 0) +
" | Caches CCT=" + caches.cctCache +
" TH=" + caches.threatCache +
" | Heals P=" + runtime.premoveHealCount +
" M=" + runtime.mainHealCount +
" A=" + runtime.analysisHealCount;
if (UI && typeof UI.updateStatusInfo === "function") {
UI.updateStatusInfo();
}
}
function getDiagnosticsSnapshot() {
const runtime = RuntimeGuard.getSnapshot();
return {
workers: {
main: !!(Engine && Engine.main),
analysis: !!(Engine && Engine.analysis),
premove: !!(Engine && Engine.premove)
},
caches: {
premoveProcessedFens: Engine && Engine._premoveProcessedFens ? Engine._premoveProcessedFens.size : 0,
cctCache: CCTAnalyzer && CCTAnalyzer.cache ? CCTAnalyzer.cache.size : 0,
threatCache: ThreatDetectionSystem && ThreatDetectionSystem.cache ? ThreatDetectionSystem.cache.size : 0
},
flags: {
loopStarted: !!State.loopStarted,
analysisMode: !!State.analysisMode,
premoveEnabled: !!State.premoveEnabled,
isThinking: !!State.isThinking,
isAnalysisThinking: !!State.isAnalysisThinking
},
errors: {
engine: ErrorTelemetry.moduleCounts.engine || 0,
ui: ErrorTelemetry.moduleCounts.ui || 0,
premove: ErrorTelemetry.moduleCounts.premove || 0,
syzygy: ErrorTelemetry.moduleCounts.syzygy || 0,
runtime: ErrorTelemetry.moduleCounts.runtime || 0,
other: ErrorTelemetry.moduleCounts.other || 0,
recent: ErrorTelemetry.recent.slice(-5)
},
runtime: {
premoveHealCount: runtime.premoveHealCount,
mainHealCount: runtime.mainHealCount,
analysisHealCount: runtime.analysisHealCount,
uiHealCount: runtime.uiHealCount,
listenerHealCount: runtime.listenerHealCount,
selfTestRuns: runtime.selfTestRuns,
selfTestFailures: runtime.selfTestFailures
}
};
}
function softResetAnalysis(reason) {
State.statusInfo = "Soft reset analysis" + (reason ? " (" + reason + ")" : "");
UI.updateStatusInfo();
if (Engine && typeof Engine.selfHealAnalysis === "function") {
Engine.selfHealAnalysis(reason || "soft-reset");
}
if (State.analysisMode) {
State._lastAnalysisFen = null;
scheduleManagedTimeout(function () {
analysisCheck();
}, 120);
}
}
function softResetPremove(reason) {
State.statusInfo = "Soft reset premove" + (reason ? " (" + reason + ")" : "");
UI.updateStatusInfo();
if (Engine && typeof Engine.selfHealPremove === "function") {
Engine.selfHealPremove(reason || "soft-reset");
}
if (typeof clearPremoveCaches === "function") {
clearPremoveCaches();
}
if (State) {
State.premoveAnalysisInProgress = false;
}
}
function runSelfTest() {
RuntimeGuard.selfTestRuns++;
let checks = [];
let failures = [];
function check(name, pass, details) {
checks.push({ name: name, pass: !!pass, details: details || "" });
if (!pass) failures.push(name + (details ? (" (" + details + ")") : ""));
}
try {
check("Board element", !!getBoardElement());
check("Main engine object", !!(Engine && Engine.main), Engine && Engine._ready ? "ready" : "not ready");
check("State object", !!State);
check("UI object", !!UI);
check("Panel exists", !!$("#chess-assist-panel"));
check("Diagnostics getter", typeof getDiagnosticsSnapshot === "function");
check("Premove engine set", !!(Engine && Engine._premoveProcessedFens));
} catch (e) {
failures.push("Exception: " + (e && e.message ? e.message : String(e)));
}
if (failures.length > 0) {
RuntimeGuard.selfTestFailures++;
State.statusInfo = "Self test FAIL: " + failures.join(" | ");
err("[SelfTest] Failures:", failures);
} else {
State.statusInfo = "Self test OK (" + checks.length + " checks)";
log("[SelfTest] All checks passed", checks);
}
UI.updateStatusInfo();
UI.updateDiagnosticsDisplay();
return { checks: checks, failures: failures };
}
function exportSettingsSnapshot() {
let payload = {
exportedAt: new Date().toISOString(),
script: (GM_info && GM_info.script && GM_info.script.name) ? GM_info.script.name : "ChessAssistant",
version: (GM_info && GM_info.script && GM_info.script.version) ? GM_info.script.version : "unknown",
settings: {}
};
Object.keys(PERSISTED_SETTING_DEFAULTS).forEach(function (key) {
payload.settings[key] = State[key];
});
return payload;
}
function exportSettingsToFile() {
try {
let payload = exportSettingsSnapshot();
let blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
let url = URL.createObjectURL(blob);
let a = document.createElement("a");
let stamp = new Date().toISOString().replace(/[:.]/g, "-");
a.href = url;
a.download = "chess-assistant-settings-" + stamp + ".json";
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
State.statusInfo = "Settings exported";
UI.updateStatusInfo();
} catch (e) {
err("Export settings failed:", e);
State.statusInfo = "Export failed";
UI.updateStatusInfo();
}
}
function importSettingsFromObject(raw) {
let src = raw && raw.settings ? raw.settings : raw;
if (!src || typeof src !== "object") {
throw new Error("Invalid settings payload");
}
Object.keys(PERSISTED_SETTING_DEFAULTS).forEach(function (key) {
if (!Object.prototype.hasOwnProperty.call(src, key)) return;
saveSetting(key, src[key]);
});
normalizeLoadedSettings();
if (Engine && Engine.main) {
if (State.evaluationMode === "human") Engine.setElo(State.eloRating);
else Engine.setFullStrength();
}
renderAll();
State.statusInfo = "Settings imported";
UI.updateStatusInfo();
}
function importSettingsFromFile(file) {
if (!file) return;
let reader = new FileReader();
reader.onload = function () {
try {
let parsed = JSON.parse(String(reader.result || "{}"));
importSettingsFromObject(parsed);
} catch (e) {
err("Import settings failed:", e);
State.statusInfo = "Import failed";
UI.updateStatusInfo();
}
};
reader.readAsText(file);
}
GM_registerMenuCommand("Run health check", runHealthCheck);
GM_registerMenuCommand("Run self test", runSelfTest);
GM_registerMenuCommand("Export settings", exportSettingsToFile);
GM_registerMenuCommand("Toggle silent logs", function () {
let nextVal = !isSilentLoggingEnabled();
saveSetting("silentLogging", nextVal);
State.statusInfo = nextVal ? "Silent logs enabled" : "Silent logs disabled";
UI.updateStatusInfo();
});
function renderAll() {
let panel = $("#chess-assist-panel");
if (!panel) return;
let contentArea = $("#cap-content-area");
let scrollTop = contentArea ? contentArea.scrollTop : 0;
let activeTab = $(".cap-tab.active")?.dataset.tab || "tab-engine";
panel.innerHTML = getPanelHTML();
setupDrag(panel);
setupMenuTabs();
setupAllListeners();
if (activeTab) {
let tab = $("[data-tab='" + activeTab + "']");
if (tab) tab.click();
}
if (scrollTop && $("#cap-content-area")) {
$("#cap-content-area").scrollTop = scrollTop;
}
if (UI && typeof UI.touchHeartbeat === "function") {
UI.touchHeartbeat();
}
UI.updateTurnLEDs();
if (UI && typeof UI.updateStatusInfo === 'function') {
UI.updateStatusInfo();
}
UI.updateClock();
UI.updatePremoveStatsDisplay();
UI.updatePremoveChanceDisplay();
if (typeof UI.updateSyzygyDisplay === "function") UI.updateSyzygyDisplay();
if (typeof UI.updateCCTDebugDisplay === "function") UI.updateCCTDebugDisplay();
if (typeof UI.updateAnalysisMonitorDisplay === "function") UI.updateAnalysisMonitorDisplay();
if (typeof UI.updateDiagnosticsDisplay === "function") UI.updateDiagnosticsDisplay();
if (State.analysisMode) UI.updateAnalysisBar(State.currentEvaluation || 0);
else UI.updateEvalBar(State.currentEvaluation || 0, null, State.customDepth);
}
function createPanel() {
if (!document.querySelector("meta[name='viewport']")) {
let viewportMeta = document.createElement("meta");
viewportMeta.name = "viewport";
viewportMeta.content = "width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes";
document.head.appendChild(viewportMeta);
}
let style = document.createElement("style");
style.id = "chess-assist-styles";
style.textContent = getPanelCSS();
document.head.appendChild(style);
let panel = document.createElement("div");
panel.id = "chess-assist-panel";
document.body.appendChild(panel);
renderAll();
if (State.panelTop !== null && State.panelLeft !== null) {
panel.style.top = State.panelTop + "px";
panel.style.left = State.panelLeft + "px";
panel.style.transform = "none";
}
applyPanelState(State.panelState);
}
function showWelcomeConsentModal(onAccept) {
if (!State.onboardingAccepted) {
saveSetting("onboardingAccepted", true);
State.onboardingAccepted = true;
}
if (typeof onAccept === "function") onAccept();
}
function setupDrag(panel) {
let handle = $(".cap-drag-handle", panel);
if (!handle || !panel) return;
if (!setupDrag._state) {
setupDrag._state = {
panel: null,
dragging: false,
offsetX: 0,
offsetY: 0
};
}
let dragState = setupDrag._state;
dragState.panel = panel;
function startDrag(clientX, clientY, target) {
if (target && target.tagName === "BUTTON") return;
let activePanel = dragState.panel;
if (!activePanel) return;
dragState.dragging = true;
let rect = activePanel.getBoundingClientRect();
dragState.offsetX = clientX - rect.left;
dragState.offsetY = clientY - rect.top;
activePanel.style.transform = "none";
activePanel.style.cursor = "grabbing";
return true;
}
function moveDrag(clientX, clientY) {
let activePanel = dragState.panel;
if (!dragState.dragging || !activePanel) return;
let x = clamp(clientX - dragState.offsetX, 0, window.innerWidth - activePanel.offsetWidth);
let y = clamp(clientY - dragState.offsetY, 0, window.innerHeight - 40);
activePanel.style.left = x + "px";
activePanel.style.top = y + "px";
}
function endDrag() {
let activePanel = dragState.panel;
if (dragState.dragging && activePanel) {
dragState.dragging = false;
activePanel.style.cursor = "grab";
let rect = activePanel.getBoundingClientRect();
saveSetting("panelTop", rect.top);
saveSetting("panelLeft", rect.left);
}
}
if (!setupDrag._docHandlersBound) {
setupDrag._docHandlersBound = true;
let docMousemoveHandler = function (e) {
moveDrag(e.clientX, e.clientY);
};
document.addEventListener("mousemove", docMousemoveHandler);
_eventListeners.push({ element: document, type: "mousemove", handler: docMousemoveHandler });
let docMouseupHandler = function () {
endDrag();
};
document.addEventListener("mouseup", docMouseupHandler);
_eventListeners.push({ element: document, type: "mouseup", handler: docMouseupHandler });
let docTouchmoveHandler = function (e) {
if (dragState.dragging && e.touches.length > 0) {
moveDrag(e.touches[0].clientX, e.touches[0].clientY);
e.preventDefault();
}
};
document.addEventListener("touchmove", docTouchmoveHandler, false);
_eventListeners.push({ element: document, type: "touchmove", handler: docTouchmoveHandler });
let docTouchendHandler = function (e) {
endDrag();
e.preventDefault();
};
document.addEventListener("touchend", docTouchendHandler);
_eventListeners.push({ element: document, type: "touchend", handler: docTouchendHandler });
}
bindElementEvent(handle, "mousedown", function (e) {
startDrag(e.clientX, e.clientY, e.target);
e.preventDefault();
}, "_bound_drag_handle_mousedown");
bindElementEvent(handle, "touchstart", function (e) {
if (e.touches.length > 0) {
startDrag(e.touches[0].clientX, e.touches[0].clientY, e.target);
e.preventDefault();
}
}, "_bound_drag_handle_touchstart");
bindElementEvent(handle, "touchmove", function (e) {
if (dragState.dragging) e.preventDefault();
}, "_bound_drag_handle_touchmove");
}
function setupMenuTabs() {
$$(".cap-tab").forEach(function (tab) {
bindElementEvent(tab, "click", function () {
let panel = $("#chess-assist-panel");
if (!panel) return;
if (panel.classList.contains("minimized")) applyPanelState("maximized");
$$(".cap-tab").forEach(function (t) {
t.classList.remove("active");
});
this.classList.add("active");
let targetId = this.dataset.tab;
$$(".cap-tab-content").forEach(function (p) {
p.style.display = "none";
});
let target = $("#" + targetId);
if (target) target.style.display = "";
if (targetId === "tab-opening") {
Syzygy.maybeProbe(getAccurateFen());
UI.updateSyzygyDisplay();
}
}, "_bound_tab_click");
});
}
function syncToggleUI(btnId, isOn) {
let btn = $("#" + btnId);
if (!btn) return;
btn.textContent = isOn ? "ON" : "OFF";
btn.classList.toggle("on", isOn);
btn.classList.toggle("off", !isOn);
}
function bindToggle(btnId, stateKey) {
let btn = $("#" + btnId);
if (!btn) return;
bindElementEvent(btn, "click", function () {
let newVal = !State[stateKey];
saveSetting(stateKey, newVal);
syncToggleUI(btnId, newVal);
if (stateKey === "showPVArrows" && !newVal) UI.clearPVArrows();
if (stateKey === "showPVArrows" && newVal) {
if (State.analysisMode && State.analysisPVLine.length > 0) {
UI.drawPVArrows(State.analysisPVLine, State.analysisPVTurn, true);
} else if (State.mainPVLine.length > 0) {
UI.drawPVArrows(State.mainPVLine, State.mainPVTurn, false);
}
}
if (stateKey === "showBestmoveArrows" && !newVal) UI.clearBestmoveArrows();
if (stateKey === "showBestmoveArrows" && newVal) UI.drawBestmoveArrows();
if (stateKey === "highlightEnabled" && !newVal) UI.clearHighlights();
if ((stateKey === "autoRun" || stateKey === "autoMovePiece") && newVal) {
if (State.analysisMode) {
saveSetting("analysisMode", false);
syncToggleUI("btn-analysis", false);
let grp = $("#analysis-colors-group");
if (grp) grp.style.display = "none";
if (Engine.analysis) {
Engine.analysis.terminate();
Engine.analysis = null;
}
}
}
if (stateKey === "autoMovePiece" && !newVal) {
restoreSmartControlsIfForced("auto-move-off");
}
if (stateKey === "autoDepthAdapt" && newVal) applyAutoDepthFromOpponent();
}, "_bound_toggle_" + btnId);
}
function bindElementEvent(el, eventType, handler, boundKey) {
if (!el || typeof el.addEventListener !== "function") return null;
let keyBase = boundKey || ("_bound_" + eventType);
let key = keyBase;
if (el[key]) return el;
el[key] = true;
el.addEventListener(eventType, function (evt) {
try {
handler.call(this, evt);
} catch (e) {
err("UI handler error:", eventType, e);
}
});
return el;
}
function bindUIEvent(selector, eventType, handler, boundKey) {
let el = $(selector);
if (!el) return null;
return bindElementEvent(el, eventType, handler, boundKey || ("_bound_" + selector + "_" + eventType));
}
function isAnalysisAutoPlayEnabled() {
return !!(State.analysisMode && State.autoAnalysisColor && State.autoAnalysisColor !== "none");
}
function setSmartControlsEnabled(enabled) {
let on = !!enabled;
saveSetting("useMainConsensus", on);
saveSetting("analysisBlunderGuard", on);
syncToggleUI("btn-main-consensus", on);
syncToggleUI("btn-analysis-blunder-guard", on);
}
let _syncSmartControlsLock = false;
function syncSmartControlsForAnalysisAutoPlay(trigger) {
if (_syncSmartControlsLock) return;
_syncSmartControlsLock = true;
try {
if (isAnalysisAutoPlayEnabled()) {
if (!State._smartControlsForcedByAutoPlay) {
State._preSmartControlsState = {
useMainConsensus: !!State.useMainConsensus,
analysisBlunderGuard: !!State.analysisBlunderGuard
};
}
State._smartControlsForcedByAutoPlay = true;
setSmartControlsEnabled(false);
return;
}
if (State._smartControlsForcedByAutoPlay) {
let prev = State._preSmartControlsState || {
useMainConsensus: true,
analysisBlunderGuard: true
};
saveSetting("useMainConsensus", !!prev.useMainConsensus);
saveSetting("analysisBlunderGuard", !!prev.analysisBlunderGuard);
syncToggleUI("btn-main-consensus", !!State.useMainConsensus);
syncToggleUI("btn-analysis-blunder-guard", !!State.analysisBlunderGuard);
State._smartControlsForcedByAutoPlay = false;
}
} finally {
_syncSmartControlsLock = false;
}
}
function restoreSmartControlsIfForced(reason) {
if (!State._smartControlsForcedByAutoPlay) return;
let prev = State._preSmartControlsState || {
useMainConsensus: true,
analysisBlunderGuard: true
};
saveSetting("useMainConsensus", !!prev.useMainConsensus);
saveSetting("analysisBlunderGuard", !!prev.analysisBlunderGuard);
syncToggleUI("btn-main-consensus", !!State.useMainConsensus);
syncToggleUI("btn-analysis-blunder-guard", !!State.analysisBlunderGuard);
State._smartControlsForcedByAutoPlay = false;
}
function setupAllListeners() {
function closeGearMenu() {
let menu = $("#cap-gear-menu");
if (menu) menu.style.display = "none";
}
bindUIEvent("#cap-gear", "click", function (e) {
if (e) e.stopPropagation();
let menu = $("#cap-gear-menu");
if (!menu) return;
menu.style.display = menu.style.display === "none" ? "flex" : "none";
});
bindUIEvent("#cap-gear-hotkeys", "click", function () {
closeGearMenu();
UI.showHotkeyHelp();
});
bindUIEvent("#cap-gear-more", "click", function () {
closeGearMenu();
let overlay = $("#cap-more-overlay");
if (overlay) overlay.style.display = "flex";
});
bindUIEvent("#cap-gear-silent", "click", function () {
let nextVal = !isSilentLoggingEnabled();
saveSetting("silentLogging", nextVal);
this.textContent = "Silent Logs: " + (nextVal ? "ON" : "OFF");
State.statusInfo = nextVal ? "Silent logs enabled" : "Silent logs disabled";
UI.updateStatusInfo();
closeGearMenu();
syncToggleUI("btn-silent-logs", nextVal);
});
bindUIEvent("#cap-more-close", "click", function () {
let overlay = $("#cap-more-overlay");
if (overlay) overlay.style.display = "none";
});
let moreBackdrop = $("#cap-more-overlay .cap-overlay-backdrop");
if (moreBackdrop) {
bindElementEvent(moreBackdrop, "click", function () {
let overlay = $("#cap-more-overlay");
if (overlay) overlay.style.display = "none";
}, "_bound_more_overlay_backdrop");
}
if (!_gearMenuDocBound) {
_gearMenuDocBound = true;
let gearMenuDocClick = function (e) {
let menu = $("#cap-gear-menu");
if (!menu || menu.style.display === "none") return;
let wrap = $(".cap-gear-wrap");
if (wrap && wrap.contains(e.target)) return;
menu.style.display = "none";
};
document.addEventListener("click", gearMenuDocClick, true);
_eventListeners.push({ element: document, type: "click", handler: gearMenuDocClick, options: true });
}
bindUIEvent("#cap-minimize", "click", function () {
applyPanelState("minimized");
});
bindUIEvent("#cap-maximize", "click", function () {
applyPanelState("maximized");
});
bindUIEvent("#cap-close", "click", function () {
applyPanelState("closed");
});
if (!_panelHotkeysBound) {
_panelHotkeysBound = true;
let panelHotkeysHandler = function (e) {
if (e.key === "Escape") {
e.preventDefault();
let newState = State.panelState === "closed" ? "maximized" : "closed";
applyPanelState(newState);
return;
}
if (State.panelState === "closed") return;
let target = e && e.target ? e.target : null;
let targetTag = target && target.tagName ? target.tagName : "";
let isInputField = ["INPUT", "SELECT", "TEXTAREA"].includes(targetTag);
let isEditable = !!(target && target.isContentEditable);
let isInEditableContainer = !!(target && target.closest && target.closest("[contenteditable]"));
if (isInputField || isEditable || isInEditableContainer) return;
if (!e.altKey) return;
let depthMap = {
q: 1, w: 2, e: 3, r: 4, t: 5, y: 6, u: 7, i: 8, o: 9, p: 10,
a: 11, s: 12, d: 13, f: 14, g: 15, h: 16, j: 17, k: 18, l: 19,
z: 20, x: 21, c: 22, v: 23, b: 24, n: 25, m: 26
};
let key = e.key.toLowerCase();
let newDepth = depthMap[key];
if (newDepth) {
e.preventDefault();
saveSetting("customDepth", newDepth);
let depthSlider = $("#sld-depth");
let depthDisplay = $("#depth-display");
if (depthSlider) depthSlider.value = State.customDepth;
if (depthDisplay) depthDisplay.textContent = State.customDepth;
if (State.analysisMode) {
State._lastAnalysisFen = null;
analysisCheck();
} else {
runEngineNow();
}
}
};
document.addEventListener("keydown", panelHotkeysHandler);
_eventListeners.push({ element: document, type: "keydown", handler: panelHotkeysHandler });
}
let quickDelayInput = $("#inp-clock-quick-delay");
if (quickDelayInput) {
bindElementEvent(quickDelayInput, "change", function () {
let val = parseInt(this.value, 10);
if (!isNaN(val) && val >= 100 && val <= 5000) {
saveSetting("clockSyncQuickDelayMs", val);
updateQuickDelayDisplay();
}
}, "_bound_quick_delay_change");
}
let lowTimeInput = $("#inp-clock-low");
if (lowTimeInput) {
bindElementEvent(lowTimeInput, "change", function () {
let v = parseInt(this.value, 10);
if (!isNaN(v) && v >= 1) {
saveSetting("clockSyncLowTimeQuickSec", v);
}
updateQuickDelayDisplay();
}, "_bound_low_time_change");
}
function updateQuickDelayDisplay() {
let threshold = $("#inp-clock-low")?.value || State.clockSyncLowTimeQuickSec;
let quickDelay = $("#inp-clock-quick-delay")?.value || State.clockSyncQuickDelayMs || 300;
let disp1 = $("#quick-threshold-display");
let disp2 = $("#quick-delay-display");
if (disp1) disp1.textContent = threshold;
if (disp2) disp2.textContent = quickDelay;
}
bindUIEvent("#btn-eval-mode", "click", function () {
let newMode = State.evaluationMode === "engine" ? "human" : "engine";
saveSetting("evaluationMode", newMode);
this.dataset.value = newMode;
if (newMode === "engine") {
this.textContent = "ENGINE";
this.classList.add("on");
this.classList.remove("off");
Engine.setFullStrength();
} else {
this.textContent = "HUMAN";
this.classList.add("off");
this.classList.remove("on");
Engine.setElo(State.eloRating);
}
let hg = $("#human-group");
let he = $("#human-elo-group");
if (hg) hg.style.display = newMode === "human" ? "" : "none";
if (he) he.style.display = newMode === "human" ? "" : "none";
});
bindUIEvent("#sel-human-level", "change", function () {
saveSetting("humanLevel", this.value);
let cfg = ELO_LEVELS[this.value];
if (cfg) {
saveSetting("eloRating", cfg.elo);
let sld = $("#sld-elo");
let disp = $("#elo-display");
if (sld) sld.value = cfg.elo;
if (disp) disp.textContent = cfg.elo;
if (State.evaluationMode === "human") Engine.setElo(cfg.elo);
}
});
bindUIEvent("#sld-elo", "input", function () {
let v = parseInt(this.value);
saveSetting("eloRating", v);
let eloDisplay = $("#elo-display");
if (eloDisplay) eloDisplay.textContent = v;
if (State.evaluationMode === "human") Engine.setElo(v);
});
bindUIEvent("#sld-depth", "input", function () {
saveSetting("customDepth", parseInt(this.value));
let depthDisplay = $("#depth-display");
if (depthDisplay) depthDisplay.textContent = State.customDepth;
});
bindUIEvent("#sld-skill", "input", function () {
let val = parseInt(this.value);
saveSetting("skillLevel", val);
let skillDisplay = $("#skill-display");
if (skillDisplay) skillDisplay.textContent = val;
Engine.setSkillLevel(val);
});
bindUIEvent("#btn-auto-depth", "click", function () {
if (State.autoDepthAdapt) {
scheduleManagedTimeout(function () {
applyAutoDepthFromOpponent();
}, 100);
}
});
bindToggle("btn-auto-run", "autoRun");
bindToggle("btn-auto-move", "autoMovePiece");
bindToggle("btn-auto-match", "autoMatch");
bindToggle("btn-highlight", "highlightEnabled");
bindToggle("btn-book", "useOpeningBook");
bindToggle("btn-auto-depth", "autoDepthAdapt");
bindToggle("btn-pv-arrows", "showPVArrows");
bindToggle("btn-bestmove-arrows", "showBestmoveArrows");
bindToggle("btn-main-consensus", "useMainConsensus");
bindToggle("btn-analysis-blunder-guard", "analysisBlunderGuard");
bindToggle("btn-silent-logs", "silentLogging");
let stableInput = $("#inp-analysis-stable");
if (stableInput) {
bindElementEvent(stableInput, "change", function () {
let v = parseInt(this.value, 10);
if (!isNaN(v)) {
saveSetting("analysisMinStableUpdates", clamp(v, 1, 5));
this.value = State.analysisMinStableUpdates;
}
}, "_bound_analysis_stable_change");
}
let pvDepthSlider = $("#sld-pv-depth");
if (pvDepthSlider) {
bindElementEvent(pvDepthSlider, "input", function () {
let v = parseInt(this.value);
saveSetting("maxPVDepth", v);
let disp = $("#pv-depth-display");
if (disp) disp.textContent = v;
if (State.showPVArrows) {
if (State.analysisMode && State.analysisPVLine.length > 0) {
UI.clearPVArrows();
UI.drawPVArrows(State.analysisPVLine, State.analysisPVTurn, true);
} else if (State.mainPVLine.length > 0) {
UI.clearPVArrows();
UI.drawPVArrows(State.mainPVLine, State.mainPVTurn, false);
}
}
}, "_bound_pv_depth_input");
}
let cctBtn = $("#btn-cct-analysis");
if (cctBtn) {
bindElementEvent(cctBtn, "click", function () {
let newVal = !State.cctAnalysisEnabled;
saveSetting("cctAnalysisEnabled", newVal);
syncToggleUI("btn-cct-analysis", newVal);
let settings = $("#cct-settings");
if (settings) settings.style.display = newVal ? "" : "none";
}, "_bound_cct_click");
}
['cct-checks', 'cct-captures', 'cct-threats'].forEach(function (id) {
let chk = $("#" + id);
if (chk) {
bindElementEvent(chk, "change", function () {
let component = id.replace('cct-', '');
State.cctComponents[component] = this.checked;
saveSetting("cctComponents", State.cctComponents);
}, "_bound_cct_component_change");
}
});
let cctDebugChk = $("#cct-debug-enabled");
if (cctDebugChk) {
bindElementEvent(cctDebugChk, "change", function () {
saveSetting("cctDebugEnabled", !!this.checked);
UI.updateCCTDebugDisplay();
}, "_bound_cct_debug_toggle");
}
bindUIEvent("#btn-analysis", "click", function () {
let newVal = !State.analysisMode;
saveSetting("analysisMode", newVal);
syncToggleUI("btn-analysis", newVal);
let grp = $("#analysis-colors-group");
if (grp) grp.style.display = newVal ? "" : "none";
if (newVal) {
State.gameEnded = false;
State.analysisGuardStateText = "Monitoring";
State._preAnalysisState = {
autoRun: State.autoRun,
autoMovePiece: State.autoMovePiece,
autoMatch: State.autoMatch,
highlightEnabled: State.highlightEnabled,
showPVArrows: State.showPVArrows
};
cancelModePendingTimers("analysis-on");
Engine.stop();
if (!State.premoveEnabled) {
saveSetting("highlightEnabled", true);
syncToggleUI("btn-highlight", true);
saveSetting("showPVArrows", true);
syncToggleUI("btn-pv-arrows", true);
}
saveSetting("autoRun", false);
syncToggleUI("btn-auto-run", false);
saveSetting("autoMovePiece", false);
syncToggleUI("btn-auto-move", false);
saveSetting("autoMatch", false);
syncToggleUI("btn-auto-match", false);
UI.clearAll();
Engine.loadAnalysisEngine();
let analysisHistoryBody = $("#moveHistoryTableBody");
if (analysisHistoryBody) analysisHistoryBody.innerHTML = "";
State.analysisHistoryCursor = 0;
State.analysisAcplFen = "";
State.analysisEvalText = "0.00";
State.analysisLastRecordedKey = "";
syncAnalysisMoveHistory();
State._lastAnalysisFen = null;
analysisCheck();
UI.updateAnalysisMonitorDisplay();
syncSmartControlsForAnalysisAutoPlay("analysis-on");
} else {
cancelModePendingTimers("analysis-off");
let prev = State._preAnalysisState || {};
saveSetting("highlightEnabled", prev.highlightEnabled !== undefined ? prev.highlightEnabled : true);
syncToggleUI("btn-highlight", State.highlightEnabled);
saveSetting("showPVArrows", prev.showPVArrows !== undefined ? prev.showPVArrows : false);
syncToggleUI("btn-pv-arrows", State.showPVArrows);
if (Engine.analysis) {
Engine.analysis.terminate();
Engine.analysis = null;
}
State.analysisPVLine = [];
State.analysisPVTurn = "w";
State.analysisStableCount = 0;
State.analysisLastBestMove = "";
State.analysisPrevEvalCp = null;
State.analysisLastEvalCp = null;
State.analysisHistoryCursor = getGameHistory().length;
State.analysisAcplFen = "";
State.analysisEvalText = "0.00";
State.analysisLastRecordedKey = "";
State.analysisGuardStateText = "Ready";
UI.clearAll();
State._lastAnalysisFen = null;
UI.updateAnalysisMonitorDisplay();
syncSmartControlsForAnalysisAutoPlay("analysis-off");
}
});
$$("[data-color]").forEach(function (btn) {
bindElementEvent(btn, "click", function () {
let color = this.dataset.color;
saveSetting("autoAnalysisColor", color);
$$("[data-color]").forEach(function (b) {
b.classList.toggle("active", b.dataset.color === color);
});
if (State.analysisMode && Engine.analysis) {
State._lastAnalysisFen = null;
analysisCheck();
}
syncSmartControlsForAnalysisAutoPlay("analysis-color-change");
}, "_bound_analysis_color_click");
});
let premoveBtn = $("#btn-premove");
if (premoveBtn) {
bindElementEvent(premoveBtn, "click", function () {
let newVal = !State.premoveEnabled;
saveSetting("premoveEnabled", newVal);
syncToggleUI("btn-premove", newVal);
cancelModePendingTimers("premove-toggle");
let settings = $("#premove-settings");
if (settings) settings.style.display = newVal ? "" : "none";
if (newVal) {
if (State.analysisMode) {
State.statusInfo = "Premove disabled in Analysis Mode";
UI.updateStatusInfo();
return;
}
State._prePremoveState = {
highlightEnabled: State.highlightEnabled,
showPVArrows: State.showPVArrows
};
saveSetting("highlightEnabled", false);
syncToggleUI("btn-highlight", false);
saveSetting("showPVArrows", false);
syncToggleUI("btn-pv-arrows", false);
UI.clearPVArrows();
UI.clearHighlights();
State.premoveLiveChance = 0;
State.premoveTargetChance = clamp(State.premoveMinConfidence || 0, 0, 100);
State.premoveLastEvalDisplay = "-";
State.premoveLastMoveDisplay = "-";
State.premoveChanceReason = "Waiting for engine PV";
State.premoveChanceUpdatedTs = 0;
UI.updatePremoveChanceDisplay();
} else {
let prev = State._prePremoveState || {};
saveSetting("highlightEnabled", prev.highlightEnabled !== undefined ? prev.highlightEnabled : true);
syncToggleUI("btn-highlight", State.highlightEnabled);
saveSetting("showPVArrows", prev.showPVArrows !== undefined ? prev.showPVArrows : false);
syncToggleUI("btn-pv-arrows", State.showPVArrows);
State.premoveLiveChance = 0;
State.premoveTargetChance = 0;
State.premoveLastEvalDisplay = "-";
State.premoveLastMoveDisplay = "-";
State.premoveChanceReason = "Disabled";
State.premoveChanceUpdatedTs = 0;
UI.updatePremoveChanceDisplay();
}
}, "_bound_premove_click");
}
let premoveModeSelect = $("#sel-premove-mode");
if (premoveModeSelect && !premoveModeSelect._bound) {
premoveModeSelect._bound = true;
bindElementEvent(premoveModeSelect, "change", function () {
saveSetting("premoveMode", this.value);
let filters = $("#premove-piece-filters");
if (filters) filters.style.display = this.value === "filter" ? "" : "none";
if (this.value !== "every" && this.value !== "capture" && this.value !== "filter") {
warn("Unknown premoveMode:", this.value);
}
}, "_bound_premove_mode_change");
}
$$("#premove-piece-filters input[type=\"checkbox\"]").forEach(function (chk) {
if (chk._bound) return;
chk._bound = true;
bindElementEvent(chk, "change", function () {
let p = this.dataset.piece;
if (!/^[qrbnp]$/.test(p)) {
warn("Unknown piece filter key:", p);
return;
}
State.premovePieces[p] = this.checked ? 1 : 0;
saveSetting("premovePieces", State.premovePieces);
}, "_bound_premove_piece_change");
});
bindUIEvent("#btn-delay-mode", "click", function () {
let newVal = !State.clockSync;
saveSetting("clockSync", newVal);
this.textContent = newVal ? "Clock Sync" : "Normal";
this.classList.toggle("on", newVal);
this.classList.toggle("off", !newVal);
let delayNormal = $("#delay-normal");
let delayFast = $("#delay-fast");
let delayPresetsGroup = $("#delay-presets-group");
if (delayNormal) delayNormal.style.display = newVal ? "none" : "";
if (delayFast) delayFast.style.display = newVal ? "" : "none";
if (delayPresetsGroup) delayPresetsGroup.style.display = newVal ? "none" : "";
let clockMinInput = $("#inp-clock-min-delay");
let clockMaxInput = $("#inp-clock-max-delay");
if (clockMinInput) clockMinInput.value = State.minDelay;
if (clockMaxInput) clockMaxInput.value = State.maxDelay;
});
let delayPresets = {
"bullet": { min: 0.5, max: 1.0 },
"blitz": { min: 1.0, max: 2.0 },
"rapid": { min: 2.0, max: 4.0 }
};
$$(".cap-preset-btn").forEach(function (btn) {
bindElementEvent(btn, "click", function () {
let preset = this.dataset.preset;
if (delayPresets[preset]) {
let config = delayPresets[preset];
saveSetting("minDelay", config.min);
saveSetting("maxDelay", config.max);
let minInput = $("#inp-min-delay");
let maxInput = $("#inp-max-delay");
let clockMinInput = $("#inp-clock-min-delay");
let clockMaxInput = $("#inp-clock-max-delay");
if (minInput) minInput.value = config.min;
if (maxInput) maxInput.value = config.max;
if (clockMinInput) clockMinInput.value = config.min;
if (clockMaxInput) clockMaxInput.value = config.max;
$$(".cap-preset-btn").forEach(function (b) {
b.classList.remove("active");
});
this.classList.add("active");
State.statusInfo = "Preset: " + preset.charAt(0).toUpperCase() + preset.slice(1) + " (" + config.min + "s - " + config.max + "s)";
UI.updateStatusInfo();
}
}, "_bound_delay_preset_click");
});
$$('[id^=\'btn-mode-\']').forEach(function (btn) {
bindElementEvent(btn, "click", function () {
let mode = this.dataset.mode;
State.moveExecutionMode = mode;
saveSetting("moveExecutionMode", mode);
let modeClickBtn = $("#btn-mode-click");
let modeDragBtn = $("#btn-mode-drag");
if (modeClickBtn) modeClickBtn.classList.toggle("active", mode === "click");
if (modeDragBtn) modeDragBtn.classList.toggle("active", mode === "drag");
State.statusInfo = "Move Mode: " + mode.toUpperCase() + (mode === "drag" ? " (Bezier)" : " (Simple)");
UI.updateStatusInfo();
}, "_bound_mode_button_click");
});
let delayInputs = {
"inp-min-delay": "minDelay",
"inp-max-delay": "maxDelay",
"inp-clock-min-delay": "minDelay",
"inp-clock-max-delay": "maxDelay"
};
Object.keys(delayInputs).forEach(function (id) {
let el = $("#" + id);
if (el) {
bindElementEvent(el, "change", function () {
let v = parseFloat(this.value);
if (!isNaN(v) && v > 0) {
saveSetting(delayInputs[id], v);
let minInput = $("#inp-min-delay");
let maxInput = $("#inp-max-delay");
let clockMinInput = $("#inp-clock-min-delay");
let clockMaxInput = $("#inp-clock-max-delay");
if (minInput) minInput.value = State.minDelay;
if (maxInput) maxInput.value = State.maxDelay;
if (clockMinInput) clockMinInput.value = State.minDelay;
if (clockMaxInput) clockMaxInput.value = State.maxDelay;
}
}, "_bound_delay_input_change");
}
});
bindUIEvent("#inp-color1", "input", function () {
saveSetting("highlightColor1", this.value);
});
let pvActiveRank = 1;
let pvActiveInput = $("#inp-pv-color-active");
if (pvActiveInput) {
bindElementEvent(pvActiveInput, "input", function () {
let colors = Object.assign({}, State.pvArrowColors || {});
colors[pvActiveRank] = this.value;
saveSetting("pvArrowColors", colors);
let swatch = $(".cap-pv-color[data-pv-rank='" + pvActiveRank + "']");
if (swatch) swatch.style.background = this.value;
if (State.showPVArrows) {
if (State.analysisMode && State.analysisPVLine.length > 0) {
UI.clearPVArrows();
UI.drawPVArrows(State.analysisPVLine, State.analysisPVTurn, true);
} else if (State.mainPVLine.length > 0) {
UI.clearPVArrows();
UI.drawPVArrows(State.mainPVLine, State.mainPVTurn, false);
}
}
}, "_bound_pv_active_input");
}
$$(".cap-pv-color").forEach(function (swatch) {
bindElementEvent(swatch, "click", function () {
let rank = parseInt(this.dataset.pvRank, 10);
if (!rank || rank < 1 || rank > 9) return;
pvActiveRank = rank;
$$(".cap-pv-color").forEach(function (el) {
el.classList.toggle("active", el.dataset.pvRank === String(rank));
});
if (pvActiveInput) {
let colors = State.pvArrowColors || {};
pvActiveInput.value = colors[rank] || colors[String(rank)] || "#4287f5";
}
}, "_bound_pv_color_click");
});
let bmActiveRank = 1;
let bmActiveInput = $("#inp-bestmove-color-active");
if (bmActiveInput) {
bindElementEvent(bmActiveInput, "input", function () {
let colors = Object.assign({}, State.bestmoveArrowColors || {});
colors[bmActiveRank] = this.value;
saveSetting("bestmoveArrowColors", colors);
if (bmActiveRank === 1) {
saveSetting("bestmoveArrowColor", this.value);
}
let swatch = $(".cap-bm-color[data-bm-rank='" + bmActiveRank + "']");
if (swatch) swatch.style.background = this.value;
if (State.showBestmoveArrows) {
UI.drawBestmoveArrows();
}
}, "_bound_bestmove_active_input");
}
$$(".cap-bm-color").forEach(function (swatch) {
bindElementEvent(swatch, "click", function () {
let rank = parseInt(this.dataset.bmRank, 10);
if (!rank || rank < 1 || rank > 9) return;
bmActiveRank = rank;
$$(".cap-bm-color").forEach(function (el) {
el.classList.toggle("active", el.dataset.bmRank === String(rank));
});
if (bmActiveInput) {
let colors = State.bestmoveArrowColors || {};
bmActiveInput.value = colors[rank] || colors[String(rank)] || "#eb6150";
}
}, "_bound_bestmove_color_click");
});
$$(".cap-preset").forEach(function (p) {
bindElementEvent(p, "click", function () {
if (!this.dataset.c) return;
let parent = this.closest(".cap-presets");
if (!parent || !parent.dataset || !parent.dataset.target) return;
let targetId = parent.dataset.target;
let input = $("#" + targetId);
if (input) {
input.value = this.dataset.c;
input.dispatchEvent(new Event("input"));
}
}, "_bound_color_preset_click");
});
bindUIEvent("#btn-reload-engine", "click", function () {
State.statusInfo = "🔄 Reloading all engines...";
UI.updateStatusInfo();
this.disabled = true;
this.textContent = "⏳";
this.style.opacity = "0.7";
Engine.reloadAllEngines().then(function (success) {
let btn = $("#btn-reload-engine");
if (btn) {
btn.disabled = false;
btn.textContent = "Reload";
btn.style.opacity = "1";
}
if (success) {
State.statusInfo = "✅ All engines reloaded!";
UI.updateStatusInfo();
if (State.analysisMode) {
State._lastAnalysisFen = null;
analysisCheck();
}
} else {
State.statusInfo = "❌ Engine reload failed";
UI.updateStatusInfo();
}
});
});
bindUIEvent("#btn-run-once", "click", function () {
if (State.analysisMode) {
State._lastAnalysisFen = null;
analysisCheck();
} else {
runEngineNow();
}
});
bindUIEvent("#btn-stop-engine", "click", function () {
Engine.stop();
if (State.analysisMode && Engine.analysis) {
Engine.analysis.postMessage("stop");
}
UI.clearAll();
});
bindUIEvent("#btn-soft-reset-analysis", "click", function () {
softResetAnalysis("ui-button");
});
bindUIEvent("#btn-soft-reset-premove", "click", function () {
softResetPremove("ui-button");
});
bindUIEvent("#btn-export-settings", "click", function () {
exportSettingsToFile();
});
bindUIEvent("#btn-import-settings", "click", function () {
let fileInput = $("#inp-import-settings");
if (fileInput) fileInput.click();
});
let importInput = $("#inp-import-settings");
if (importInput) {
bindElementEvent(importInput, "change", function () {
let file = this.files && this.files.length ? this.files[0] : null;
importSettingsFromFile(file);
this.value = "";
}, "_bound_import_settings_change");
}
bindUIEvent("#inp-move-filter", "input", function () {
UI.applyMoveHistoryFilter(this.value || "");
});
bindUIEvent("#btn-clear-history", "click", function () {
MoveHistory.clear();
});
bindUIEvent("#btn-auto-resign", "click", function () {
let newVal = !State.autoResignEnabled;
saveSetting("autoResignEnabled", newVal);
syncToggleUI("btn-auto-resign", newVal);
let autoResignGroup = $("#auto-resign-group");
if (autoResignGroup) autoResignGroup.style.display = newVal ? "" : "none";
});
bindUIEvent("#sel-resign-mode", "change", function () {
saveSetting("resignMode", this.value);
let resignMateBox = $("#resign-mate-box");
let resignCpBox = $("#resign-cp-box");
if (resignMateBox) resignMateBox.style.display = this.value === "mate" ? "" : "none";
if (resignCpBox) resignCpBox.style.display = this.value === "cp" ? "" : "none";
});
bindUIEvent("#sel-resign-m", "change", function () {
let v = parseInt(this.value);
if (!isNaN(v)) saveSetting("autoResignThresholdMate", v);
});
bindUIEvent("#inp-resign-cp", "change", function () {
let v = parseInt(this.value);
if (!isNaN(v)) saveSetting("autoResignThresholdCp", v);
});
bindUIEvent("#btn-clock-sync", "click", function () {
let newVal = !State.clockSync;
saveSetting("clockSync", newVal);
syncToggleUI("btn-clock-sync", newVal);
let clockSyncGroup = $("#clock-sync-group");
if (clockSyncGroup) clockSyncGroup.style.display = newVal ? "" : "none";
});
bindUIEvent("#txt-notation-sequence", "input", function () {
saveSetting("notationSequence", this.value.trim());
});
syncSmartControlsForAnalysisAutoPlay("setup-listeners");
}
function loadStockfishManually() {
return EngineLoader.loadAsync();
}
// =====================================================
// Section 15: Board and Game State Functions
// =====================================================
function getBoardElement() {
return $("wc-chess-board") || $("chess-board") || $(".board");
}
function getGameController() {
try {
let board = getBoardElement();
if (!board) return null;
if (board.game && typeof board.game === 'object') return board.game;
if (board._game && typeof board._game === 'object') return board._game;
if (typeof unsafeWindow !== 'undefined' && unsafeWindow.chesscom && unsafeWindow.chesscom.game) {
return unsafeWindow.chesscom.game;
}
} catch (e) {
}
return null;
}
function getGameHistory() {
let gc = getGameController();
if (!gc) return [];
try {
let log = (typeof gc.getLog === "function") ? gc.getLog() : (gc.log || []);
if (Array.isArray(log)) {
return log.map(m => m.uci || m.move?.uci).filter(m => !!m);
}
} catch (e) {
warn("Error getting history:", e);
}
return [];
}
function getGame() {
let now = Date.now();
if (cachedGame && (now - cachedGameTimestamp) < GAME_CACHE_TTL) {
try {
if (typeof cachedGame.getFEN === "function") {
cachedGame.getFEN();
return cachedGame;
}
} catch (e) {
cachedGame = null;
}
}
cachedGame = getGameController();
cachedGameTimestamp = now;
return cachedGame;
}
function normalizeSide(val) {
if (val === 1 || val === "w" || val === "white") return "w";
if (val === 2 || val === "b" || val === "black") return "b";
return null;
}
function getPlayingAs(game) {
let g = game || getGameController();
if (!g) return null;
try {
if (typeof g.getPlayingAs === "function") return normalizeSide(g.getPlayingAs());
} catch (e) { }
return null;
}
function detectPlayersTurnFromDOM() {
try {
let selectors = [
".clock-time-monospace[role='timer']",
".clock-time-monospace",
".clock-component .clock-time-monospace"
];
let clocks = [];
for (let i = 0; i < selectors.length; i++) {
let found = Array.from(document.querySelectorAll(selectors[i])).filter(function (el) {
return !!(el && el.offsetParent !== null);
});
if (found.length >= 2) {
clocks = found;
break;
}
}
if (clocks.length < 2) return null;
let activeClock = clocks.find(function (el) {
let cls = String((el.closest(".clock-component") || el).className || "").toLowerCase();
return cls.includes("player-turn") || cls.includes("clock-player-turn");
});
if (!activeClock) return null;
let sorted = clocks
.map(function (el) { return { el: el, rect: el.getBoundingClientRect() }; })
.sort(function (a, b) { return a.rect.top - b.rect.top; });
let bottomClock = sorted[sorted.length - 1].el;
return activeClock === bottomClock;
} catch (e) {
return null;
}
}
function isPlayersTurn(game) {
let g = game || getGame();
if (!g) {
let domTurn = detectPlayersTurnFromDOM();
return domTurn === null ? false : domTurn;
}
try {
let turn, playingAs;
if (typeof g.getTurn === "function") turn = g.getTurn();
if (typeof g.getPlayingAs === "function") playingAs = g.getPlayingAs();
let normTurn = normalizeSide(turn);
let normPlaying = normalizeSide(playingAs);
if (normTurn !== null && normPlaying !== null) return normTurn === normPlaying;
} catch (e) { }
let domTurn = detectPlayersTurnFromDOM();
return domTurn === null ? false : domTurn;
}
function isBoardFlipped() {
let board = getBoardElement();
if (!board) return false;
return board.classList.contains("flipped") || board.getAttribute("data-flipped") === "true";
}
function getAccurateFen() {
let game = getGameController();
if (game) {
try {
if (typeof game.getFEN === "function") return game.getFEN();
if (typeof game.fen === "function") return game.fen();
if (game.fen && typeof game.fen === "string") return game.fen;
} catch (e) { }
}
return buildFenFromDOM();
}
function hashFen(fen) {
if (!fen || typeof fen !== "string") return "";
return fen.split(' ').slice(0, 4).join(' ');
}
function normalizeFen(fen) {
if (!fen) return "";
let parts = fen.split(" ");
return parts.slice(0, 4).join(" ");
}
function getCurrentTurn(fen) {
if (!fen) return "w";
let parts = fen.split(" ");
return parts.length > 1 ? parts[1] : "w";
}
function updateMoveNumber(fen) {
if (!fen) return;
let parts = fen.split(" ");
if (parts.length >= 6) {
State.moveNumber = parseInt(parts[5], 10) || 1;
}
}
function saveSetting(key, value) {
if (!Object.prototype.hasOwnProperty.call(State, key)) return;
const sanitized = sanitizeSettingValue(key, value);
State[key] = sanitized;
GM_setValue(key, sanitized);
}
// =====================================================
// Section 16: FEN Construction from DOM
// =====================================================
function buildFenFromDOM() {
let board = getBoardElement();
if (!board) return null;
let grid = [];
let r, c;
for (r = 0; r < 8; r++) {
grid[r] = [];
for (c = 0; c < 8; c++) {
grid[r][c] = null;
}
}
let pieces = $$(".piece", board);
if (pieces.length === 0) return null;
pieces.forEach(function (piece) {
let classes = piece.className.split(/\s+/);
let pieceType = null;
let squareStr = null;
for (let i = 0; i < classes.length; i++) {
if (PIECE_CHAR[classes[i]]) pieceType = PIECE_CHAR[classes[i]];
if (/^square-\d{2,}$/.test(classes[i])) squareStr = classes[i].replace("square-", "");
}
if (pieceType && squareStr) {
let file = parseInt(squareStr.charAt(0)) - 1;
let rank = parseInt(squareStr.charAt(1)) - 1;
if (file >= 0 && file < 8 && rank >= 0 && rank < 8) {
grid[7 - rank][file] = pieceType;
}
}
});
let fenRows = [];
for (r = 0; r < 8; r++) {
let row = "";
let empty = 0;
for (c = 0; c < 8; c++) {
if (grid[r][c]) {
if (empty > 0) {
row += empty;
empty = 0;
}
row += grid[r][c];
} else {
empty++;
}
}
if (empty > 0) row += empty;
fenRows.push(row);
}
let turn = "w";
let moveList = $$(".move-node, .move, [data-ply]");
if (moveList.length > 0) turn = moveList.length % 2 === 0 ? "w" : "b";
let castling = "";
if (grid[7][4] === "K" && grid[7][7] === "R") castling += "K";
if (grid[7][4] === "K" && grid[7][0] === "R") castling += "Q";
if (grid[0][4] === "k" && grid[0][7] === "r") castling += "k";
if (grid[0][4] === "k" && grid[0][0] === "r") castling += "q";
if (!castling) castling = "-";
return fenRows.join("/") + " " + turn + " " + castling + " - 0 1";
}
// =====================================================
// Section 17: FEN and Piece Manipulation
// =====================================================
function fenCharAtSquare(fen, square) {
if (!fen || !square) return null;
let placement = fen.split(" ")[0];
let ranks = placement.split("/");
let file = "abcdefgh".indexOf(square[0]);
let rankNum = parseInt(square[1], 10);
if (file < 0 || rankNum < 1 || rankNum > 8 || ranks.length !== 8) return null;
let row = 8 - rankNum;
let rowStr = ranks[row];
let col = 0;
for (let i = 0; i < rowStr.length; i++) {
let ch = rowStr[i];
if (/\d/.test(ch)) {
col += parseInt(ch, 10);
if (col > file) return null;
} else {
if (col === file) return ch;
col++;
}
}
return null;
}
function pieceFromFenChar(ch) {
if (!ch) return null;
let isUpper = ch === ch.toUpperCase();
return {
color: isUpper ? "w" : "b",
type: ch.toLowerCase()
};
}
function findKing(fen, color) {
let placement = fen.split(" ")[0];
let ranks = placement.split("/");
let kingChar = color === "w" ? "K" : "k";
for (let rankIdx = 0; rankIdx < 8; rankIdx++) {
let rank = 8 - rankIdx;
let file = 0;
for (let i = 0; i < ranks[rankIdx].length; i++) {
let ch = ranks[rankIdx][i];
if (/\d/.test(ch)) {
file += parseInt(ch, 10);
} else {
if (ch === kingChar) return "abcdefgh"[file] + rank;
file++;
}
}
}
return null;
}
function isEnPassantCapture(fen, from, to, ourColor) {
let parts = fen.split(" ");
let ep = parts[3];
let fromPiece = pieceFromFenChar(fenCharAtSquare(fen, from));
if (!fromPiece || fromPiece.color !== ourColor || fromPiece.type !== "p") return false;
return ep && ep !== "-" && to === ep && from[0] !== to[0];
}
function makeSimpleMove(fen, from, to, promotion) {
if (!fen || !from || !to) return fen;
try {
let parts = fen.split(" ");
let ranks = parts[0].split("/");
let fromFile = from.charCodeAt(0) - 97;
let fromRank = 8 - parseInt(from[1], 10);
let toFile = to.charCodeAt(0) - 97;
let toRank = 8 - parseInt(to[1], 10);
if (fromFile < 0 || fromFile > 7 || toFile < 0 || toFile > 7 ||
fromRank < 0 || fromRank > 7 || toRank < 0 || toRank > 7) return fen;
let expand = function (r) {
return r.replace(/\d/g, function (d) {
return ".".repeat(+d);
});
};
let compress = function (r) {
return r.replace(/\.{1,8}/g, function (m) {
return "" + m.length;
});
};
let board = ranks.map(function (r) {
return expand(r).split("");
});
let piece = board[fromRank][fromFile];
if (!piece || piece === ".") return fen;
let isPawn = piece.toLowerCase() === "p";
let isKing = piece.toLowerCase() === "k";
let isCapture = board[toRank][toFile] !== ".";
if (isPawn && parts[3] && parts[3] !== "-" && to === parts[3]) {
let epRank = piece === "P" ? toRank + 1 : toRank - 1;
if (epRank >= 0 && epRank < 8) {
board[epRank][toFile] = ".";
isCapture = true;
}
}
board[fromRank][fromFile] = ".";
if (isPawn && (toRank === 0 || toRank === 7)) {
let promoChar = promotion || "q";
board[toRank][toFile] = piece === piece.toUpperCase() ? promoChar.toUpperCase() : promoChar.toLowerCase();
} else {
board[toRank][toFile] = piece;
}
if (isKing && Math.abs(fromFile - toFile) === 2) {
let rookFromFile = toFile > fromFile ? 7 : 0;
let rookToFile = toFile > fromFile ? toFile - 1 : toFile + 1;
board[toRank][rookToFile] = board[toRank][rookFromFile];
board[toRank][rookFromFile] = ".";
}
parts[0] = board.map(function (r) {
return compress(r.join(""));
}).join("/");
let currentSide = parts[1] || "w";
parts[1] = currentSide === "w" ? "b" : "w";
let castling = parts[2] || "-";
if (castling !== "-") {
if (isKing) {
if (piece === 'K') castling = castling.replace(/[KQ]/g, '');
else castling = castling.replace(/[kq]/g, '');
}
if (from === 'a1' || to === 'a1') castling = castling.replace('Q', '');
if (from === 'h1' || to === 'h1') castling = castling.replace('K', '');
if (from === 'a8' || to === 'a8') castling = castling.replace('q', '');
if (from === 'h8' || to === 'h8') castling = castling.replace('k', '');
if (castling === '') castling = '-';
}
parts[2] = castling;
if (isPawn && Math.abs(fromRank - toRank) === 2) {
let epRankNum = 8 - ((fromRank + toRank) / 2);
parts[3] = "abcdefgh"[fromFile] + epRankNum;
} else {
parts[3] = "-";
}
let halfmove = parseInt(parts[4] || "0", 10);
if (isPawn || isCapture) halfmove = 0;
else halfmove++;
parts[4] = "" + halfmove;
if (parts[1] === "w") {
parts[5] = "" + (parseInt(parts[5] || "1", 10) + 1);
}
return parts.join(" ");
} catch (e) {
return fen;
}
}
function getPredictedFen(fen, pvMoves) {
if (!pvMoves || pvMoves.length === 0) return fen;
let predictedFen = fen;
let oppMove = pvMoves[0];
if (oppMove && oppMove.length >= 4) {
let oppFrom = oppMove.substring(0, 2);
let oppTo = oppMove.substring(2, 4);
let oppPromo = oppMove.length > 4 ? oppMove[4] : null;
predictedFen = makeSimpleMove(predictedFen, oppFrom, oppTo, oppPromo);
}
return predictedFen;
}
// =====================================================
// Section 18: Attack and Threat Detection
// =====================================================
function getAttackersOfSquare(fen, targetSquare, attackerColor) {
let attackers = [];
let tFile = "abcdefgh".indexOf(targetSquare[0]);
let tRank = parseInt(targetSquare[1], 10);
if (tFile < 0 || tRank < 1 || tRank > 8) return attackers;
let checkSquare = function (file, rank, pieceTypes) {
if (file < 0 || file > 7 || rank < 1 || rank > 8) return;
let sq = "abcdefgh"[file] + rank;
let ch = fenCharAtSquare(fen, sq);
let p = pieceFromFenChar(ch);
if (p && p.color === attackerColor && pieceTypes.includes(p.type)) {
attackers.push({ square: sq, piece: p.type });
}
};
let pawnDir = attackerColor === "w" ? 1 : -1;
checkSquare(tFile - 1, tRank - pawnDir, ["p"]);
checkSquare(tFile + 1, tRank - pawnDir, ["p"]);
let knightMoves = [
[2, 1],
[2, -1],
[-2, 1],
[-2, -1],
[1, 2],
[1, -2],
[-1, 2],
[-1, -2]
];
knightMoves.forEach(function (m) {
checkSquare(tFile + m[0], tRank + m[1], ["n"]);
});
for (let df = -1; df <= 1; df++) {
for (let dr = -1; dr <= 1; dr++) {
if (df === 0 && dr === 0) continue;
checkSquare(tFile + df, tRank + dr, ["k"]);
}
}
let directions = [
{ dx: 1, dy: 0, pieces: ["r", "q"] }, { dx: -1, dy: 0, pieces: ["r", "q"] },
{ dx: 0, dy: 1, pieces: ["r", "q"] }, { dx: 0, dy: -1, pieces: ["r", "q"] },
{ dx: 1, dy: 1, pieces: ["b", "q"] }, { dx: 1, dy: -1, pieces: ["b", "q"] },
{ dx: -1, dy: 1, pieces: ["b", "q"] }, { dx: -1, dy: -1, pieces: ["b", "q"] }
];
directions.forEach(function (dir) {
let f = tFile + dir.dx;
let r = tRank + dir.dy;
while (f >= 0 && f <= 7 && r >= 1 && r <= 8) {
let sq = "abcdefgh"[f] + r;
let ch = fenCharAtSquare(fen, sq);
if (ch) {
let p = pieceFromFenChar(ch);
if (p && p.color === attackerColor && dir.pieces.includes(p.type)) {
attackers.push({ square: sq, piece: p.type });
}
break;
}
f += dir.dx;
r += dir.dy;
}
});
return attackers;
}
function isSquareAttackedBy(fen, square, attackerColor) {
return getAttackersOfSquare(fen, square, attackerColor).length > 0;
}
function isCheckmate(fen, colorInCheck) {
let kingPos = findKing(fen, colorInCheck);
if (!kingPos) return false;
let oppColor = colorInCheck === "w" ? "b" : "w";
if (!isSquareAttackedBy(fen, kingPos, oppColor)) return false;
let kf = "abcdefgh".indexOf(kingPos[0]);
let kr = parseInt(kingPos[1]);
for (let df = -1; df <= 1; df++) {
for (let dr = -1; dr <= 1; dr++) {
if (df === 0 && dr === 0) continue;
let nf = kf + df,
nr = kr + dr;
if (nf < 0 || nf > 7 || nr < 1 || nr > 8) continue;
let sq = "abcdefgh"[nf] + nr;
let ch = fenCharAtSquare(fen, sq);
let piece = pieceFromFenChar(ch);
if (piece && piece.color === colorInCheck) continue;
let testFen = makeSimpleMove(fen, kingPos, sq);
let newKingPos = sq;
if (!isSquareAttackedBy(testFen, newKingPos, oppColor)) {
return false;
}
}
}
return true;
}
// =====================================================
// Section 19: Premove Safety Check
// =====================================================
const PremoveSafety = {
cache: new Map(),
CACHE_DURATION: 1000,
RISK: {
CRITICAL: 100,
VERY_HIGH: 80,
HIGH: 60,
MEDIUM: 40,
LOW: 20,
SAFE: 0
},
PIECE_RISK: {
q: 10,
r: 8,
b: 5,
n: 5,
p: 3
},
check(fen, uci, ourColor) {
if (!fen || !uci || uci.length < 4) {
return this._createResult(false, "Invalid move", this.RISK.CRITICAL, null);
}
const cacheKey = `${fen}|${uci}`;
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
return cached.result;
}
const from = uci.substring(0, 2);
const to = uci.substring(2, 4);
const oppColor = ourColor === "w" ? "b" : "w";
const movingCh = fenCharAtSquare(fen, from);
const movingPiece = pieceFromFenChar(movingCh);
if (!movingPiece || movingPiece.color !== ourColor) {
return this._createResult(false, "Not our piece", this.RISK.CRITICAL, null);
}
const newFen = makeSimpleMove(fen, from, to);
if (!newFen) {
return this._createResult(false, "Invalid move", this.RISK.CRITICAL, null);
}
const ourKingPos = findKing(newFen, ourColor);
if (ourKingPos && isSquareAttackedBy(newFen, ourKingPos, oppColor)) {
return this._createResult(false, "Exposes king to check", this.RISK.CRITICAL, null);
}
const oppKingPos = findKing(newFen, oppColor);
const givesCheck = oppKingPos && isSquareAttackedBy(newFen, oppKingPos, ourColor);
if (givesCheck && isCheckmate(newFen, oppColor)) {
return this._createResult(true, "Checkmate!", this.RISK.SAFE, null);
}
const cct = State.cctAnalysisEnabled
? analyzeCCT(fen, uci, ourColor)
: null;
const analysis = this._analyzeSafety(fen, newFen, from, to, movingPiece, ourColor, oppColor, givesCheck, cct);
const result = this._createResult(
analysis.safe,
analysis.reasons.join(", ") || (analysis.safe ? "Safe" : "Risky"),
analysis.riskLevel,
cct
);
updateCCTDebugSnapshot("PremoveSafety", uci, cct, result, result.reason);
this.cache.set(cacheKey, {
result: result,
timestamp: Date.now()
});
if (this.cache.size > 100) {
const entries = [...this.cache.entries()];
const cutoff = Date.now() - this.CACHE_DURATION;
entries.forEach(([key, value]) => {
if (value.timestamp < cutoff) {
this.cache.delete(key);
}
});
}
return result;
},
_analyzeSafety(fen, newFen, from, to, movingPiece, ourColor, oppColor, givesCheck, cct) {
let riskLevel = 0;
const reasons = [];
const destCh = fenCharAtSquare(fen, to);
const destPiece = pieceFromFenChar(destCh);
if (cct && cct.givesCheck) {
if (cct.checkIsSafe) {
reasons.push("✓ Safe check");
riskLevel -= 15;
} else {
reasons.push("⚠ Check (may be recaptured)");
riskLevel -= 5;
}
}
if (cct && cct.captureAnalysis) {
const netGain = cct.captureAnalysis.netMaterialGain;
if (netGain < 0) {
const lossPenalty = givesCheck ? 8 : 12;
reasons.push(`✗ Loses material: ${netGain}`);
riskLevel += Math.abs(netGain) * lossPenalty;
} else if (netGain > 0) {
reasons.push(`✓ Wins material: +${netGain}`);
riskLevel -= netGain * 3;
}
if (cct.captureAnalysis.ourPieceHanging && !destPiece) {
if (!givesCheck) {
const pieceValue = PIECE_VALUES[movingPiece.type] || 0;
riskLevel += 25 + (pieceValue * 5);
reasons.push(`✗ Piece hanging (${movingPiece.type})`);
} else {
reasons.push("⚠ Piece exposed but gives check");
riskLevel += 10;
}
}
}
if (cct && cct.threats) {
if (cct.threats.created && cct.threats.created.length > 0) {
const majorThreats = cct.threats.created.filter(t => t.severity === 'high');
if (majorThreats.length > 0) {
reasons.push(`✓ Creates ${majorThreats.length} major threat(s)`);
riskLevel -= 8 * Math.min(majorThreats.length, 2);
}
}
if (cct.threats.weFallInto && cct.threats.weFallInto.length > 0) {
const highThreats = cct.threats.weFallInto.filter(t => t.severity === 'high');
const mediumThreats = cct.threats.weFallInto.filter(t => t.severity === 'medium');
if (highThreats.length > 0) {
if (!givesCheck) {
reasons.push(`✗ Falls into ${highThreats.length} HIGH threat(s)`);
riskLevel += 50 * highThreats.length;
} else {
reasons.push(`⚠ HIGH threats but gives check`);
riskLevel += 20 * highThreats.length;
}
}
if (mediumThreats.length > 0 && !givesCheck) {
reasons.push(`⚠ Falls into ${mediumThreats.length} medium threat(s)`);
riskLevel += 20 * mediumThreats.length;
}
}
}
if (movingPiece.type === "k") {
if (isSquareAttackedBy(fen, to, oppColor)) {
reasons.push("✗ CRITICAL: King into check");
riskLevel = this.RISK.CRITICAL;
}
}
if (movingPiece.type !== "k") {
const ourKingPos = findKing(fen, ourColor);
if (ourKingPos && isPiecePinned(fen, from, ourKingPos, ourColor, oppColor)) {
reasons.push("✗ Piece is PINNED to king");
riskLevel += 70;
if (!givesCheck) {
reasons.push("✗ CRITICAL: Illegal move");
riskLevel = this.RISK.CRITICAL;
}
}
}
if (movingPiece.type === "q") {
const queenRisk = this._analyzeQueenRisk(newFen, to, oppColor, ourColor, destPiece, givesCheck, cct);
riskLevel += queenRisk.risk;
reasons.push(...queenRisk.reasons);
}
if (movingPiece.type === "r") {
const rookRisk = this._analyzeRookRisk(newFen, to, oppColor, ourColor, destPiece, givesCheck, cct);
riskLevel += rookRisk.risk;
reasons.push(...rookRisk.reasons);
}
if (movingPiece.type === "p") {
const pawnBonus = this._analyzePawnAdvancement(to, ourColor, cct);
riskLevel += pawnBonus.risk;
reasons.push(...pawnBonus.reasons);
}
if (!destPiece && !givesCheck) {
const hangingRisk = this._analyzeHangingPiece(newFen, to, oppColor, ourColor, movingPiece, cct);
riskLevel += hangingRisk.risk;
reasons.push(...hangingRisk.reasons);
}
riskLevel = Math.max(-25, Math.min(100, riskLevel));
const safe = riskLevel < 30;
return {
safe,
riskLevel: Math.max(0, riskLevel),
reasons
};
},
_analyzeQueenRisk(newFen, to, oppColor, ourColor, destPiece, givesCheck, cct) {
let risk = 0;
const reasons = [];
const attackers = getAttackersOfSquare(newFen, to, oppColor);
const defenders = getAttackersOfSquare(newFen, to, ourColor);
if (attackers.length > 0 && !destPiece) {
if (cct && cct.threats && cct.threats.weFallInto) {
const queenTrapped = cct.threats.weFallInto.some(t => t.type === 'queen_trapped');
if (queenTrapped) {
if (!givesCheck) {
reasons.push("✗ QUEEN TRAP!");
risk = 95;
} else {
reasons.push("⚠ Queen may be trapped but gives check");
risk += 40;
}
return { risk, reasons };
}
}
if (!givesCheck) {
const exchangePenalty = defenders.length === 0 ? 60 : 45;
risk += exchangePenalty + (attackers.length * 15);
const hasCounterplay = cct && cct.threats && cct.threats.created && cct.threats.created.length > 0;
if (!hasCounterplay) {
reasons.push("✗ Undefended QUEEN - HIGH RISK!");
} else {
reasons.push("⚠ Undefended queen but has counterplay");
}
} else if (defenders.length === 0) {
risk += 35;
reasons.push("⚠ Queen undefended but gives check");
} else {
risk += 20;
reasons.push("⚠ Queen exposed but defended & checks");
}
if (attackers.some(a => a.piece === 'r')) {
risk += 25;
reasons.push("⚠ Queen exposed to enemy rook");
}
}
return { risk, reasons };
},
_analyzeRookRisk(newFen, to, oppColor, ourColor, destPiece, givesCheck, cct) {
let risk = 0;
const reasons = [];
const attackers = getAttackersOfSquare(newFen, to, oppColor);
const defenders = getAttackersOfSquare(newFen, to, ourColor);
if (attackers.length > 0) {
const captureValue = destPiece ? PIECE_VALUES[destPiece.type] : 0;
if (cct && cct.captureAnalysis && cct.captureAnalysis.exchangeResult < 0) {
if (!givesCheck) {
risk += 50;
reasons.push("✗ Bad exchange for rook");
} else {
risk += 25;
reasons.push("⚠ Rook exchange but gives check");
}
}
if (defenders.length === 0) {
risk += 40;
reasons.push(`✗ Rook UNDEFENDED (${attackers.length} attackers)`);
} else if (captureValue < 5) {
const hasCounterplay = cct && cct.threats && cct.threats.created && cct.threats.created.length > 0;
if (!hasCounterplay && !givesCheck) {
risk += 40;
reasons.push("✗ Rook exposed without compensation");
}
}
}
return { risk, reasons };
},
_analyzePawnAdvancement(to, ourColor, cct) {
let risk = 0;
const reasons = [];
const promoRank = ourColor === "w" ? 8 : 1;
const currentRank = parseInt(to[1]);
const distanceToPromo = Math.abs(currentRank - promoRank);
if (currentRank === promoRank) {
reasons.push("✓ Promotes!");
risk -= 20;
return { risk, reasons };
}
if (distanceToPromo <= 2) {
if (cct && cct.threats && cct.threats.created) {
const hasPromoThreat = cct.threats.created.some(t => t.type === 'promotion_threat');
if (hasPromoThreat) {
reasons.push("✓ Promotion threat");
risk -= 15;
}
}
}
return { risk, reasons };
},
_analyzeHangingPiece(newFen, to, oppColor, ourColor, movingPiece, cct) {
let risk = 0;
const reasons = [];
const attackers = getAttackersOfSquare(newFen, to, oppColor);
const defenders = getAttackersOfSquare(newFen, to, ourColor);
if (attackers.length > 0) {
if (defenders.length === 0) {
const pieceCoefficient = this.PIECE_RISK[movingPiece.type] || 5;
risk += 20 + (attackers.length * pieceCoefficient);
const hasCounterplay = cct && cct.threats && cct.threats.created && cct.threats.created.length > 0;
if (!hasCounterplay) {
reasons.push(`✗ Undefended ${movingPiece.type.toUpperCase()} - HIGH RISK!`);
} else {
reasons.push(`⚠ Undefended ${movingPiece.type.toUpperCase()} but has counterplay`);
}
} else if (defenders.length === 1 && attackers.length >= 2) {
risk += 20;
reasons.push("⚠ Piece may be captured in exchange");
}
}
return { risk, reasons };
},
_createResult(safe, reason, riskLevel, cct) {
return {
safe: safe,
reason: reason,
riskLevel: Math.max(0, Math.min(100, riskLevel)),
cct: cct
};
},
clearCache() {
this.cache.clear();
}
};
function checkPremoveSafety(fen, uci, ourColor) {
return PremoveSafety.check(fen, uci, ourColor);
}
// =====================================================
// Section 20: Checks, Captures, and Threats Analysis (CCT)
// =====================================================
const CCTAnalyzer = {
cache: new Map(),
CACHE_DURATION: 1000,
analyze(fen, uci, ourColor) {
if (!fen || !uci || uci.length < 4 || !ourColor) {
return this._createEmptyResult();
}
const cacheKey = `${fen}|${uci}|${ourColor}`;
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
return cached.result;
}
const from = uci.substring(0, 2);
const to = uci.substring(2, 4);
const oppColor = ourColor === "w" ? "b" : "w";
const movingCh = fenCharAtSquare(fen, from);
const movingPiece = pieceFromFenChar(movingCh);
if (!movingPiece || movingPiece.color !== ourColor) {
return this._createEmptyResult();
}
const capturedCh = fenCharAtSquare(fen, to);
const capturedPiece = pieceFromFenChar(capturedCh);
const newFen = makeSimpleMove(fen, from, to);
if (!newFen) {
return this._createEmptyResult();
}
const result = {
givesCheck: false,
checkIsSafe: false,
captureAnalysis: null,
threats: { created: [], weFallInto: [], prevented: [] }
};
const oppKingPos = findKing(newFen, oppColor);
if (oppKingPos) {
result.givesCheck = isSquareAttackedBy(newFen, oppKingPos, ourColor);
if (result.givesCheck) {
result.checkIsSafe = this._isSafeCheck(fen, newFen, from, to, movingPiece, oppColor);
}
}
result.captureAnalysis = this._analyzeCaptures(
fen, newFen, from, to, movingPiece, capturedPiece, oppColor
);
result.threats = this._analyzeThreats(
fen, newFen, from, to, movingPiece, capturedPiece, ourColor, oppColor
);
this.cache.set(cacheKey, {
result: result,
timestamp: Date.now()
});
if (this.cache.size > 300) {
const cutoff = Date.now() - this.CACHE_DURATION;
for (const [k, v] of this.cache.entries()) {
if (v.timestamp < cutoff) {
this.cache.delete(k);
}
}
if (this.cache.size > 250) {
const overflow = this.cache.size - 250;
const keys = Array.from(this.cache.keys()).slice(0, overflow);
keys.forEach((k) => this.cache.delete(k));
}
}
return result;
},
_isSafeCheck(oldFen, newFen, from, to, movingPiece, oppColor) {
const attackers = getAttackersOfSquare(newFen, to, oppColor);
if (attackers.length === 0) return true;
const ourValue = PIECE_VALUES[movingPiece.type] || 0;
let minAttackerValue = Infinity;
attackers.forEach(a => {
const v = PIECE_VALUES[a.piece] || 0;
if (v < minAttackerValue) {
minAttackerValue = v;
}
});
return minAttackerValue >= ourValue;
},
_analyzeCaptures(oldFen, newFen, from, to, movingPiece, capturedPiece, oppColor) {
const result = {
isCapture: !!capturedPiece,
capturedValue: capturedPiece ? (PIECE_VALUES[capturedPiece.type] || 0) : 0,
ourPieceHanging: false,
exchangeResult: 0,
netMaterialGain: 0
};
const ourPieceValue = PIECE_VALUES[movingPiece.type] || 0;
const newAttackers = getAttackersOfSquare(newFen, to, oppColor);
const newDefenders = getAttackersOfSquare(newFen, to, movingPiece.color);
if (newAttackers.length > 0) {
result.ourPieceHanging = newDefenders.length === 0;
const attackerValues = newAttackers.map(a => PIECE_VALUES[a.piece] || 0);
const lowestAttacker = Math.min(...attackerValues);
if (result.isCapture) {
if (result.ourPieceHanging) {
result.netMaterialGain = result.capturedValue - ourPieceValue;
} else {
if (lowestAttacker < ourPieceValue) {
result.netMaterialGain = result.capturedValue - ourPieceValue;
} else {
result.netMaterialGain = result.capturedValue - (newDefenders.length > 0 ? 0 : ourPieceValue);
}
}
} else {
if (result.ourPieceHanging) {
result.netMaterialGain = -ourPieceValue;
}
}
} else {
result.netMaterialGain = result.capturedValue;
}
result.exchangeResult = result.netMaterialGain;
return result;
},
_analyzeThreats(oldFen, newFen, from, to, movingPiece, capturedPiece, ourColor, oppColor) {
const threats = {
created: [],
weFallInto: [],
prevented: []
};
const forks = this._detectForks(newFen, to, movingPiece.type, ourColor, oppColor);
threats.created.push(...forks);
const discovered = this._detectDiscoveredAttack(oldFen, newFen, from, to, ourColor, oppColor);
if (discovered) {
threats.created.push(discovered);
}
const pin = this._detectPinPotential(newFen, to, movingPiece.type, ourColor, oppColor);
if (pin) {
threats.created.push(pin);
}
if (movingPiece.type === 'p') {
const promoThreat = this._detectPromotionThreat(to, ourColor);
if (promoThreat) {
threats.created.push(promoThreat);
}
}
const backRank = this._detectBackRankThreat(newFen, movingPiece.type, ourColor, oppColor);
if (backRank) {
threats.created.push(backRank);
}
const oppForks = this._detectOpponentForks(newFen, ourColor, oppColor);
threats.weFallInto.push(...oppForks);
if (movingPiece.type === 'q') {
const queenTrap = this._detectQueenTrap(newFen, to, ourColor, oppColor);
if (queenTrap) {
threats.weFallInto.push(queenTrap);
}
}
const leftBehind = this._detectLeftBehind(oldFen, newFen, from, ourColor, oppColor);
threats.weFallInto.push(...leftBehind);
const oppPins = this._detectOpponentPins(newFen, ourColor, oppColor);
threats.weFallInto.push(...oppPins);
const prevented = this._detectPreventedThreats(oldFen, newFen, from, to, ourColor, oppColor);
threats.prevented.push(...prevented);
return threats;
},
_detectForks(fen, square, pieceType, ourColor, oppColor) {
const forks = [];
const attacked = this._getAttackedPieces(fen, square, ourColor, oppColor);
if (attacked.length >= 2) {
const totalValue = attacked.reduce((sum, p) => sum + (PIECE_VALUES[p.type] || 0), 0);
const hasKing = attacked.some(p => p.type === 'k');
const hasQueen = attacked.some(p => p.type === 'q');
const severity = hasKing ? 'high' : (hasQueen || totalValue >= 8) ? 'high' : 'medium';
forks.push({
type: 'fork',
severity: severity,
targets: attacked.map(p => p.square),
value: totalValue,
description: `Fork attacking ${attacked.length} pieces (value: ${totalValue})`
});
}
return forks;
},
_detectDiscoveredAttack(oldFen, newFen, from, to, ourColor, oppColor) {
const longRangePieces = getAllPieces(oldFen, ourColor).filter(p =>
p.type === 'q' || p.type === 'r' || p.type === 'b'
);
for (const piece of longRangePieces) {
if (piece.square === from) continue;
const attackedBefore = this._getAttackedSquares(oldFen, piece.square, piece.type, ourColor);
const attackedAfter = this._getAttackedSquares(newFen, piece.square, piece.type, ourColor);
const newlyAttacked = attackedAfter.filter(sq => !attackedBefore.includes(sq));
for (const sq of newlyAttacked) {
const targetPiece = pieceFromFenChar(fenCharAtSquare(newFen, sq));
if (targetPiece && targetPiece.color === oppColor) {
const value = PIECE_VALUES[targetPiece.type] || 0;
return {
type: 'discovered_attack',
severity: value >= 5 ? 'high' : 'medium',
target: sq,
attacker: piece.square,
value: value,
description: `Discovered attack on ${targetPiece.type} at ${sq}`
};
}
}
}
return null;
},
_detectPinPotential(fen, square, pieceType, ourColor, oppColor) {
if (!['q', 'r', 'b'].includes(pieceType)) return null;
const directions = this._getPieceDirections(pieceType);
const oppKing = findKing(fen, oppColor);
if (!oppKing) return null;
for (const dir of directions) {
const ray = this._castRay(fen, square, dir);
if (ray.length >= 2) {
const lastSquare = ray[ray.length - 1];
if (lastSquare === oppKing && ray.length === 2) {
const pinnedSquare = ray[0];
const pinnedPiece = pieceFromFenChar(fenCharAtSquare(fen, pinnedSquare));
if (pinnedPiece && pinnedPiece.color === oppColor) {
return {
type: 'pin',
severity: 'medium',
target: pinnedSquare,
description: `Pinning ${pinnedPiece.type} to king`
};
}
}
}
}
return null;
},
_detectPromotionThreat(to, ourColor) {
const promoRank = ourColor === "w" ? 8 : 1;
const currentRank = parseInt(to[1]);
const distance = Math.abs(currentRank - promoRank);
if (distance <= 2) {
return {
type: 'promotion_threat',
severity: distance === 1 ? 'high' : 'medium',
distance: distance,
description: `Pawn ${distance} square(s) from promotion`
};
}
return null;
},
_detectBackRankThreat(fen, pieceType, ourColor, oppColor) {
if (pieceType !== 'r' && pieceType !== 'q') return null;
const backRank = oppColor === "w" ? "1" : "8";
const oppKing = findKing(fen, oppColor);
if (oppKing && oppKing[1] === backRank) {
const escape = this._canKingEscape(fen, oppKing, oppColor);
if (!escape) {
return {
type: 'back_rank',
severity: 'high',
description: 'Back rank mate threat'
};
}
}
return null;
},
_detectOpponentForks(fen, ourColor, oppColor) {
const forks = [];
const oppPieces = getAllPieces(fen, oppColor);
for (const piece of oppPieces) {
const attacked = this._getAttackedPieces(fen, piece.square, oppColor, ourColor);
if (attacked.length >= 2) {
const hasKing = attacked.some(p => p.type === 'k');
const hasQueen = attacked.some(p => p.type === 'q');
const totalValue = attacked.reduce((sum, p) => sum + (PIECE_VALUES[p.type] || 0), 0);
forks.push({
type: 'opponent_fork',
severity: hasKing ? 'high' : (hasQueen || totalValue >= 8) ? 'high' : 'medium',
attacker: piece.square,
targets: attacked.map(p => p.square),
description: `Opponent ${piece.type} forks ${attacked.length} pieces`
});
}
}
return forks;
},
_detectQueenTrap(fen, queenSquare, ourColor, oppColor) {
const queenMoves = this._getQueenMoves(fen, queenSquare, ourColor);
const safeSquares = queenMoves.filter(sq => {
const testFen = makeSimpleMove(fen, queenSquare, sq);
return !isSquareAttackedBy(testFen, sq, oppColor);
});
if (safeSquares.length <= 2) {
return {
type: 'queen_trapped',
severity: 'high',
escapeSquares: safeSquares.length,
description: `Queen has only ${safeSquares.length} safe escape(s)`
};
}
return null;
},
_detectLeftBehind(oldFen, newFen, from, ourColor, oppColor) {
const threats = [];
const ourPieces = getAllPieces(newFen, ourColor);
for (const piece of ourPieces) {
if (piece.square === from) continue;
const wasDefended = getAttackersOfSquare(oldFen, piece.square, ourColor).length > 0;
const isDefendedNow = getAttackersOfSquare(newFen, piece.square, ourColor).length > 0;
const isAttacked = getAttackersOfSquare(newFen, piece.square, oppColor).length > 0;
if (wasDefended && !isDefendedNow && isAttacked) {
const value = PIECE_VALUES[piece.type] || 0;
threats.push({
type: 'undefended_piece',
severity: value >= 5 ? 'high' : 'medium',
square: piece.square,
pieceType: piece.type,
value: value,
description: `${piece.type} at ${piece.square} left undefended`
});
}
}
return threats;
},
_detectOpponentPins(fen, ourColor, oppColor) {
const pins = [];
const ourKing = findKing(fen, ourColor);
if (!ourKing) return pins;
const oppLongRange = getAllPieces(fen, oppColor).filter(p =>
p.type === 'q' || p.type === 'r' || p.type === 'b'
);
for (const piece of oppLongRange) {
const directions = this._getPieceDirections(piece.type);
for (const dir of directions) {
const ray = this._castRay(fen, piece.square, dir);
if (ray.length >= 2 && ray[ray.length - 1] === ourKing) {
const pinnedSquare = ray[0];
const pinnedPiece = pieceFromFenChar(fenCharAtSquare(fen, pinnedSquare));
if (pinnedPiece && pinnedPiece.color === ourColor) {
pins.push({
type: 'opponent_pin',
severity: 'medium',
pinnedPiece: pinnedSquare,
description: `Our ${pinnedPiece.type} pinned by opponent ${piece.type}`
});
}
}
}
}
return pins;
},
_detectPreventedThreats(oldFen, newFen, from, to, ourColor, oppColor) {
const prevented = [];
const capturedPiece = pieceFromFenChar(fenCharAtSquare(oldFen, to));
if (capturedPiece && capturedPiece.color === oppColor) {
const threats = this._getPieceThreats(oldFen, to, oppColor, ourColor);
if (threats.length > 0) {
prevented.push({
type: 'threat_removed',
severity: 'medium',
description: `Removed threatening ${capturedPiece.type}`
});
}
}
const blocked = this._detectBlockedAttack(oldFen, newFen, to, ourColor, oppColor);
if (blocked) {
prevented.push(blocked);
}
return prevented;
},
_getAttackedPieces(fen, square, attackerColor, defenderColor) {
const attacked = [];
const moves = this._getPossibleMoves(fen, square, attackerColor);
for (const move of moves) {
const piece = pieceFromFenChar(fenCharAtSquare(fen, move));
if (piece && piece.color === defenderColor) {
attacked.push({ square: move, type: piece.type });
}
}
return attacked;
},
_getAttackedSquares(fen, square, pieceType, color) {
return this._getPossibleMoves(fen, square, color);
},
_getPossibleMoves(fen, square, color) {
const piece = pieceFromFenChar(fenCharAtSquare(fen, square));
if (!piece) return [];
const allSquares = [];
for (let file = 0; file < 8; file++) {
for (let rank = 1; rank <= 8; rank++) {
const sq = "abcdefgh"[file] + rank;
allSquares.push(sq);
}
}
return allSquares.filter(sq => {
const testFen = makeSimpleMove(fen, square, sq);
return testFen !== fen;
});
},
_getPieceDirections(pieceType) {
switch (pieceType) {
case 'q': return [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1]];
case 'r': return [[1, 0], [-1, 0], [0, 1], [0, -1]];
case 'b': return [[1, 1], [1, -1], [-1, 1], [-1, -1]];
default: return [];
}
},
_castRay(fen, square, direction) {
const ray = [];
const [dx, dy] = direction;
let file = "abcdefgh".indexOf(square[0]);
let rank = parseInt(square[1]);
while (true) {
file += dx;
rank += dy;
if (file < 0 || file > 7 || rank < 1 || rank > 8) break;
const sq = "abcdefgh"[file] + rank;
const piece = fenCharAtSquare(fen, sq);
ray.push(sq);
if (piece && piece !== '.') break;
}
return ray;
},
_canKingEscape(fen, kingSquare, color) {
const kingMoves = this._getKingMoves(kingSquare);
const oppColor = color === "w" ? "b" : "w";
for (const move of kingMoves) {
const piece = fenCharAtSquare(fen, move);
const pieceObj = pieceFromFenChar(piece);
if (pieceObj && pieceObj.color === color) continue;
const testFen = makeSimpleMove(fen, kingSquare, move);
if (!isSquareAttackedBy(testFen, move, oppColor)) {
return true;
}
}
return false;
},
_getKingMoves(square) {
const moves = [];
const file = "abcdefgh".indexOf(square[0]);
const rank = parseInt(square[1]);
const offsets = [
[-1, -1], [-1, 0], [-1, 1],
[0, -1], [0, 1],
[1, -1], [1, 0], [1, 1]
];
for (const [dx, dy] of offsets) {
const newFile = file + dx;
const newRank = rank + dy;
if (newFile >= 0 && newFile <= 7 && newRank >= 1 && newRank <= 8) {
moves.push("abcdefgh"[newFile] + newRank);
}
}
return moves;
},
_getQueenMoves(fen, square, color) {
const moves = [];
const directions = this._getPieceDirections('q');
for (const dir of directions) {
const ray = this._castRay(fen, square, dir);
moves.push(...ray);
}
return moves;
},
_getPieceThreats(fen, square, pieceColor, targetColor) {
const attacked = this._getAttackedPieces(fen, square, pieceColor, targetColor);
return attacked.filter(p => (PIECE_VALUES[p.type] || 0) >= 3);
},
_detectBlockedAttack(oldFen, newFen, blockSquare, ourColor, oppColor) {
const oppLongRange = getAllPieces(oldFen, oppColor).filter(p =>
p.type === 'q' || p.type === 'r' || p.type === 'b'
);
for (const piece of oppLongRange) {
const directions = this._getPieceDirections(piece.type);
for (const dir of directions) {
const rayBefore = this._castRay(oldFen, piece.square, dir);
const rayAfter = this._castRay(newFen, piece.square, dir);
if (rayAfter.includes(blockSquare) && rayBefore.length > rayAfter.length) {
return {
type: 'blocked_attack',
severity: 'medium',
description: `Blocked ${piece.type} attack`
};
}
}
}
return null;
},
_createEmptyResult() {
return {
givesCheck: false,
checkIsSafe: false,
captureAnalysis: {
isCapture: false,
capturedValue: 0,
ourPieceHanging: false,
exchangeResult: 0,
netMaterialGain: 0
},
threats: { created: [], weFallInto: [], prevented: [] }
};
},
clearCache() {
this.cache.clear();
}
};
function analyzeCCT(fen, uci, ourColor) {
return CCTAnalyzer.analyze(fen, uci, ourColor);
}
function updateCCTDebugSnapshot(stage, uci, cct, safety, extra) {
if (!State) return;
let safeStage = String(stage || "CCT");
let safeUci = String(uci || "-").toUpperCase();
let checkText = cct && cct.givesCheck ? (cct.checkIsSafe ? "check:safe" : "check:risky") : "check:no";
let capText = "cap:0";
if (cct && cct.captureAnalysis) {
capText = "cap:" + (cct.captureAnalysis.netMaterialGain || 0);
}
let threatText = "th:0/0";
if (cct && cct.threats) {
let created = Array.isArray(cct.threats.created) ? cct.threats.created.length : 0;
let danger = Array.isArray(cct.threats.weFallInto) ? cct.threats.weFallInto.length : 0;
threatText = "th:" + created + "/" + danger;
}
let riskText = "risk:-";
if (safety && typeof safety.riskLevel === "number") {
riskText = "risk:" + Math.round(safety.riskLevel);
}
let extraText = extra ? (" | " + String(extra)) : "";
State.cctLastDebugText = "[" + safeStage + "] " + safeUci + " | " + checkText + " | " + capText + " | " + threatText + " | " + riskText + extraText;
if (State.cctDebugEnabled && UI && typeof UI.updateCCTDebugDisplay === "function") {
UI.updateCCTDebugDisplay();
}
}
// =====================================================
// Section 21: Threat Detection System
// =====================================================
const ThreatDetectionSystem = {
cache: new Map(),
CACHE_TTL: 1000,
detectForks(fen, square, pieceType, ourColor) {
const cacheKey = `fork|${fen}|${square}|${pieceType}|${ourColor}`;
const cached = this._getCache(cacheKey);
if (cached) return cached;
const oppColor = ourColor === "w" ? "b" : "w";
const threats = [];
if (!['n', 'q', 'p', 'b', 'r', 'k'].includes(pieceType)) {
return this._setCache(cacheKey, threats);
}
const attackSquares = getSquaresAttackedByPiece(fen, square, pieceType, ourColor);
const attackedPieces = [];
for (const sq of attackSquares) {
const ch = fenCharAtSquare(fen, sq);
const piece = pieceFromFenChar(ch);
if (piece && piece.color === oppColor) {
const value = PIECE_VALUES[piece.type] || 0;
if (value >= 3 || piece.type === 'k') {
attackedPieces.push({
square: sq,
type: piece.type,
value: value
});
}
}
}
if (attackedPieces.length >= 2) {
const totalValue = attackedPieces.reduce((sum, p) => sum + p.value, 0);
const hasKing = attackedPieces.some(p => p.type === 'k');
const hasQueen = attackedPieces.some(p => p.type === 'q');
threats.push({
type: 'fork',
severity: hasKing ? 'high' : (hasQueen || totalValue >= 10 ? 'high' : 'medium'),
attacker: pieceType,
attackerSquare: square,
targets: attackedPieces,
totalValue: totalValue,
description: `${pieceType.toUpperCase()} fork on ${attackedPieces.map(p => p.type.toUpperCase()).join(" and ")}`
});
}
return this._setCache(cacheKey, threats);
},
detectDiscoveredAttack(oldFen, newFen, from, to, ourColor) {
const cacheKey = `disco|${oldFen}|${from}|${to}`;
const cached = this._getCache(cacheKey);
if (cached !== undefined) return cached;
const oppColor = ourColor === "w" ? "b" : "w";
const ourPieces = getAllPieces(newFen, ourColor);
const oppPieces = getAllPieces(newFen, oppColor);
const longRangePieces = ourPieces.filter(p =>
['q', 'r', 'b'].includes(p.type) && p.square !== to
);
const valuablePieces = oppPieces.filter(p =>
(PIECE_VALUES[p.type] || 0) >= 5 || p.type === 'k'
);
for (const ourPiece of longRangePieces) {
for (const oppPiece of valuablePieces) {
if (canPieceAttackSquare(newFen, ourPiece, oppPiece.square)) {
if (wasBlocked(oldFen, from, ourPiece.square, oppPiece.square)) {
const result = {
type: 'discovered_attack',
severity: oppPiece.type === 'k' ? 'high' : 'medium',
attacker: ourPiece.type,
attackerSquare: ourPiece.square,
target: oppPiece.type,
targetSquare: oppPiece.square,
value: PIECE_VALUES[oppPiece.type] || 0,
description: `Discovered attack: ${ourPiece.type.toUpperCase()} -> ${oppPiece.type.toUpperCase()}`
};
return this._setCache(cacheKey, result);
}
}
}
}
return this._setCache(cacheKey, null);
},
detectPinPotential(fen, square, pieceType, ourColor) {
const cacheKey = `pin|${fen}|${square}|${pieceType}`;
const cached = this._getCache(cacheKey);
if (cached !== undefined) return cached;
if (!['q', 'r', 'b'].includes(pieceType)) {
return this._setCache(cacheKey, null);
}
const oppColor = ourColor === "w" ? "b" : "w";
const oppKing = findKing(fen, oppColor);
if (!oppKing) return this._setCache(cacheKey, null);
const directions = this._getPinDirections(pieceType);
const [tFile, tRank] = this._parseSquare(square);
for (const dir of directions) {
let f = tFile + dir.dx;
let r = tRank + dir.dy;
const piecesInLine = [];
while (f >= 0 && f <= 7 && r >= 1 && r <= 8) {
const sq = "abcdefgh"[f] + r;
const ch = fenCharAtSquare(fen, sq);
if (ch && ch !== '.') {
const piece = pieceFromFenChar(ch);
if (!piece) break;
piecesInLine.push({ square: sq, piece: piece });
if (piecesInLine.length === 2) {
const [first, second] = piecesInLine;
if (first.piece.color === oppColor &&
second.piece.color === oppColor &&
second.piece.type === 'k' &&
(PIECE_VALUES[first.piece.type] || 0) >= 3) {
const result = {
type: 'pin',
severity: 'medium',
pinner: pieceType,
pinnerSquare: square,
pinnedPiece: first.piece.type,
pinnedSquare: first.square,
description: `${pieceType.toUpperCase()} pins ${first.piece.type.toUpperCase()} to king`
};
return this._setCache(cacheKey, result);
}
break;
}
}
f += dir.dx;
r += dir.dy;
}
}
return this._setCache(cacheKey, null);
},
detectOpponentForks(fen, ourColor) {
const cacheKey = `oppfork|${fen}|${ourColor}`;
const cached = this._getCache(cacheKey);
if (cached) return cached;
const oppColor = ourColor === "w" ? "b" : "w";
const threats = [];
const oppPieces = getAllPieces(fen, oppColor);
for (const oppPiece of oppPieces) {
if (oppPiece.type === 'p') continue;
const attacked = getSquaresAttackedByPiece(fen, oppPiece.square, oppPiece.type, oppColor);
const ourAttackedPieces = [];
for (const sq of attacked) {
const ch = fenCharAtSquare(fen, sq);
const piece = pieceFromFenChar(ch);
if (piece && piece.color === ourColor) {
const value = PIECE_VALUES[piece.type] || 0;
if (value >= 1) {
ourAttackedPieces.push({
square: sq,
type: piece.type,
value: value
});
}
}
}
if (ourAttackedPieces.length >= 2) {
const totalValue = ourAttackedPieces.reduce((sum, p) => sum + p.value, 0);
const hasKing = ourAttackedPieces.some(p => p.type === 'k');
const hasQueen = ourAttackedPieces.some(p => p.type === 'q');
threats.push({
type: 'opponent_fork',
severity: hasKing ? 'high' : (hasQueen || totalValue >= 10 ? 'high' : 'medium'),
attacker: oppPiece.type,
attackerSquare: oppPiece.square,
targets: ourAttackedPieces,
totalValue: totalValue,
description: `Opponent ${oppPiece.type.toUpperCase()} forks ${ourAttackedPieces.map(p => p.type.toUpperCase()).join(" and ")}`
});
}
}
const ourPieces = getAllPieces(fen, ourColor);
for (const ourPiece of ourPieces) {
const value = PIECE_VALUES[ourPiece.type] || 0;
if (value < 5) continue;
const attackers = getAttackersOfSquare(fen, ourPiece.square, oppColor);
if (attackers.length >= 2) {
threats.push({
type: 'multiple_attackers',
severity: 'high',
target: ourPiece.type,
targetSquare: ourPiece.square,
attackerCount: attackers.length,
attackers: attackers.map(a => ({ type: a.piece, square: a.square })),
description: `${ourPiece.type.toUpperCase()} attacked ${attackers.length} times`
});
}
}
return this._setCache(cacheKey, threats);
},
detectQueenTrap(fen, queenSquare, ourColor) {
const cacheKey = `qtrap|${fen}|${queenSquare}`;
const cached = this._getCache(cacheKey);
if (cached !== undefined) return cached;
const oppColor = ourColor === "w" ? "b" : "w";
const escapeSquares = getQueenEscapeSquares(fen, queenSquare, ourColor);
const safeEscapes = escapeSquares.filter(sq => {
const attackers = getAttackersOfSquare(fen, sq, oppColor);
return attackers.length === 0;
});
const nearbyOppPieces = getPiecesInRadius(fen, queenSquare, 2, oppColor);
const [file, rank] = this._parseSquare(queenSquare);
const isEdge = file === 0 || file === 7 || rank === 1 || rank === 8;
const isCorner = (file === 0 || file === 7) && (rank === 1 || rank === 8);
const isTrapped =
(isCorner && safeEscapes.length <= 1) ||
(isEdge && safeEscapes.length <= 1 && nearbyOppPieces.length >= 3) ||
(!isEdge && safeEscapes.length <= 2 && nearbyOppPieces.length >= 4);
return this._setCache(cacheKey, isTrapped);
},
detectLeftBehind(oldFen, newFen, from, to, ourColor) {
const cacheKey = `leftbehind|${oldFen}|${from}|${to}`;
const cached = this._getCache(cacheKey);
if (cached) return cached;
const threats = [];
const oppColor = ourColor === "w" ? "b" : "w";
const ourPieces = getAllPieces(oldFen, ourColor);
for (const piece of ourPieces) {
if (piece.square === from) continue;
const wasDefended = isSquareDefendedBy(oldFen, piece.square, from);
if (wasDefended) {
const stillDefendedByMovedPiece = isSquareDefendedBy(newFen, piece.square, to);
const hasOtherDefenders = getAttackersOfSquare(newFen, piece.square, ourColor).length > 0;
const stillDefended = stillDefendedByMovedPiece || hasOtherDefenders;
if (!stillDefended) {
const attackers = getAttackersOfSquare(newFen, piece.square, oppColor);
if (attackers.length > 0) {
const value = PIECE_VALUES[piece.type] || 0;
threats.push({
type: 'left_undefended',
severity: value >= 5 ? 'high' : 'medium',
piece: piece.type,
square: piece.square,
value: value,
attackers: attackers.length,
description: `${piece.type.toUpperCase()} left undefended at ${piece.square}`
});
}
}
}
}
return this._setCache(cacheKey, threats);
},
detectPreventedThreats(oldFen, newFen, uci, ourColor) {
const cacheKey = `prevented|${oldFen}|${uci}`;
const cached = this._getCache(cacheKey);
if (cached) return cached;
const prevented = [];
const oppColor = ourColor === "w" ? "b" : "w";
const to = uci.substring(2, 4);
const capturedPiece = pieceFromFenChar(fenCharAtSquare(oldFen, to));
if (capturedPiece && capturedPiece.color === oppColor) {
const capturedThreats = this._getPieceThreats(oldFen, to, oppColor, ourColor);
if (capturedThreats > 0) {
prevented.push({
type: 'threat_removed',
severity: 'medium',
removedPiece: capturedPiece.type,
description: `Removed threatening ${capturedPiece.type.toUpperCase()}`
});
}
}
const oppPieces = getAllPieces(newFen, oppColor);
for (const oppPiece of oppPieces) {
const wasAttacked = isSquareAttackedBy(oldFen, oppPiece.square, ourColor);
const nowAttacked = isSquareAttackedBy(newFen, oppPiece.square, ourColor);
if (!wasAttacked && nowAttacked) {
const value = PIECE_VALUES[oppPiece.type] || 0;
if (value >= 3) {
prevented.push({
type: 'new_attack',
severity: value >= 5 ? 'high' : 'medium',
target: oppPiece.type,
square: oppPiece.square,
value: value,
description: `Now attacking ${oppPiece.type.toUpperCase()} at ${oppPiece.square}`
});
}
}
}
const blocked = this._detectBlockedAttack(oldFen, newFen, to, ourColor, oppColor);
if (blocked) {
prevented.push(blocked);
}
return this._setCache(cacheKey, prevented);
},
_getPieceThreats(fen, square, pieceColor, targetColor) {
const attacked = getSquaresAttackedByPiece(fen, square,
pieceFromFenChar(fenCharAtSquare(fen, square)).type, pieceColor);
let threatCount = 0;
for (const sq of attacked) {
const piece = pieceFromFenChar(fenCharAtSquare(fen, sq));
if (piece && piece.color === targetColor) {
const value = PIECE_VALUES[piece.type] || 0;
if (value >= 3) threatCount++;
}
}
return threatCount;
},
_detectBlockedAttack(oldFen, newFen, blockSquare, ourColor, oppColor) {
const oppLongRange = getAllPieces(oldFen, oppColor).filter(p =>
['q', 'r', 'b'].includes(p.type)
);
for (const piece of oppLongRange) {
const directions = this._getPinDirections(piece.type);
for (const dir of directions) {
const rayBefore = this._castRay(oldFen, piece.square, dir);
const rayAfter = this._castRay(newFen, piece.square, dir);
if (rayAfter.includes(blockSquare) && rayBefore.length > rayAfter.length) {
return {
type: 'blocked_attack',
severity: 'medium',
blockedPiece: piece.type,
description: `Blocked ${piece.type.toUpperCase()} attack`
};
}
}
}
return null;
},
_getPinDirections(pieceType) {
const directions = [];
if (pieceType === 'r' || pieceType === 'q') {
directions.push(
{ dx: 1, dy: 0 }, { dx: -1, dy: 0 },
{ dx: 0, dy: 1 }, { dx: 0, dy: -1 }
);
}
if (pieceType === 'b' || pieceType === 'q') {
directions.push(
{ dx: 1, dy: 1 }, { dx: 1, dy: -1 },
{ dx: -1, dy: 1 }, { dx: -1, dy: -1 }
);
}
return directions;
},
_castRay(fen, square, direction) {
const ray = [];
const [startFile, startRank] = this._parseSquare(square);
let f = startFile + direction.dx;
let r = startRank + direction.dy;
while (f >= 0 && f <= 7 && r >= 1 && r <= 8) {
const sq = "abcdefgh"[f] + r;
ray.push(sq);
const ch = fenCharAtSquare(fen, sq);
if (ch && ch !== '.') break;
f += direction.dx;
r += direction.dy;
}
return ray;
},
_parseSquare(square) {
return [
"abcdefgh".indexOf(square[0]),
parseInt(square[1])
];
},
_getCache(key) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
return cached.value;
}
return undefined;
},
_setCache(key, value) {
this.cache.set(key, { value, timestamp: Date.now() });
if (this.cache.size > 200) {
const cutoff = Date.now() - this.CACHE_TTL;
for (const [k, v] of this.cache.entries()) {
if (v.timestamp < cutoff) {
this.cache.delete(k);
}
}
}
return value;
},
clearCache() {
this.cache.clear();
}
};
function getSquaresAttackedByPiece(fen, square, pieceType, color) {
let squares = [];
let f = "abcdefgh".indexOf(square[0]);
let r = parseInt(square[1]);
if (pieceType === 'n') {
let moves = [
[2, 1],
[2, -1],
[-2, 1],
[-2, -1],
[1, 2],
[1, -2],
[-1, 2],
[-1, -2]
];
moves.forEach(function (m) {
let nf = f + m[0],
nr = r + m[1];
if (nf >= 0 && nf <= 7 && nr >= 1 && nr <= 8) {
squares.push("abcdefgh"[nf] + nr);
}
});
}
if (pieceType === 'p') {
let dir = color === 'w' ? 1 : -1;
[-1, 1].forEach(function (df) {
let nf = f + df,
nr = r + dir;
if (nf >= 0 && nf <= 7 && nr >= 1 && nr <= 8) {
squares.push("abcdefgh"[nf] + nr);
}
});
}
if (['q', 'r', 'b'].includes(pieceType)) {
let directions = [];
if (pieceType === 'q' || pieceType === 'r') {
directions.push([1, 0], [-1, 0], [0, 1], [0, -1]);
}
if (pieceType === 'q' || pieceType === 'b') {
directions.push([1, 1], [1, -1], [-1, 1], [-1, -1]);
}
directions.forEach(function (d) {
let nf = f + d[0],
nr = r + d[1];
while (nf >= 0 && nf <= 7 && nr >= 1 && nr <= 8) {
let sq = "abcdefgh"[nf] + nr;
squares.push(sq);
if (fenCharAtSquare(fen, sq)) break;
nf += d[0];
nr += d[1];
}
});
}
if (pieceType === 'k') {
for (let df = -1; df <= 1; df++) {
for (let dr = -1; dr <= 1; dr++) {
if (df === 0 && dr === 0) continue;
let nf = f + df,
nr = r + dr;
if (nf >= 0 && nf <= 7 && nr >= 1 && nr <= 8) {
squares.push("abcdefgh"[nf] + nr);
}
}
}
}
return squares;
}
function getAllPieces(fen, color) {
let pieces = [];
let placement = fen.split(" ")[0];
let ranks = placement.split("/");
for (let rankIdx = 0; rankIdx < 8; rankIdx++) {
let rank = 8 - rankIdx;
let file = 0;
for (let i = 0; i < ranks[rankIdx].length; i++) {
let ch = ranks[rankIdx][i];
if (/\d/.test(ch)) {
file += parseInt(ch, 10);
} else {
let isUpper = ch === ch.toUpperCase();
let pieceColor = isUpper ? 'w' : 'b';
if (pieceColor === color) {
pieces.push({ square: "abcdefgh"[file] + rank, type: ch.toLowerCase(), char: ch, color: pieceColor });
}
file++;
}
}
}
return pieces;
}
function canPieceAttackSquare(fen, piece, targetSquare) {
let squares = getSquaresAttackedByPiece(fen, piece.square, piece.type, piece.color);
return squares.includes(targetSquare);
}
function wasBlocked(fen, movedFrom, attackerSquare, targetSquare) {
let af = "abcdefgh".indexOf(attackerSquare[0]);
let ar = parseInt(attackerSquare[1]);
let tf = "abcdefgh".indexOf(targetSquare[0]);
let tr = parseInt(targetSquare[1]);
let mf = "abcdefgh".indexOf(movedFrom[0]);
let mr = parseInt(movedFrom[1]);
let df = tf - af;
let dr = tr - ar;
if (df === 0 && dr === 0) return false;
if (df !== 0 && dr !== 0 && Math.abs(df) !== Math.abs(dr)) return false;
let stepF = df === 0 ? 0 : (df > 0 ? 1 : -1);
let stepR = dr === 0 ? 0 : (dr > 0 ? 1 : -1);
if (mf === af && mr === ar) return false;
if (mf === tf && mr === tr) return false;
let dmf = mf - af;
let dmr = mr - ar;
if (stepF === 0) {
if (mf !== af) return false;
if (stepR > 0) {
if (!(mr > ar && mr < tr)) return false;
} else {
if (!(mr < ar && mr > tr)) return false;
}
} else if (stepR === 0) {
if (mr !== ar) return false;
if (stepF > 0) {
if (!(mf > af && mf < tf)) return false;
} else {
if (!(mf < af && mf > tf)) return false;
}
} else {
if (Math.abs(dmf) !== Math.abs(dmr)) return false;
let movedStepF = dmf > 0 ? 1 : -1;
let movedStepR = dmr > 0 ? 1 : -1;
if (movedStepF !== stepF || movedStepR !== stepR) return false;
if (Math.abs(dmf) >= Math.abs(df)) return false;
}
return true;
}
function isSquareDefendedBy(fen, square, defenderSquare) {
let ch = fenCharAtSquare(fen, defenderSquare);
let piece = pieceFromFenChar(ch);
if (!piece) return false;
let attacked = getSquaresAttackedByPiece(fen, defenderSquare, piece.type, piece.color);
return attacked.includes(square);
}
function isPiecePinned(fen, pieceSquare, kingSquare, ourColor, oppColor) {
const pf = "abcdefgh".indexOf(pieceSquare[0]);
const pr = parseInt(pieceSquare[1], 10);
const kf = "abcdefgh".indexOf(kingSquare[0]);
const kr = parseInt(kingSquare[1], 10);
const df = kf - pf;
const dr = kr - pr;
if (df !== 0 && dr !== 0 && Math.abs(df) !== Math.abs(dr)) return false;
const stepF = df === 0 ? 0 : -(df / Math.abs(df));
const stepR = dr === 0 ? 0 : -(dr / Math.abs(dr));
let f = pf + stepF;
let r = pr + stepR;
while (f >= 0 && f <= 7 && r >= 1 && r <= 8) {
const sq = "abcdefgh"[f] + r;
const ch = fenCharAtSquare(fen, sq);
if (ch) {
const p = pieceFromFenChar(ch);
if (!p || p.color === ourColor) return false;
if (
((stepF === 0 || stepR === 0) && (p.type === 'r' || p.type === 'q')) ||
((stepF !== 0 && stepR !== 0) && (p.type === 'b' || p.type === 'q'))
) {
return true;
}
return false;
}
f += stepF;
r += stepR;
}
return false;
}
function getQueenEscapeSquares(fen, queenSquare, color) {
let escapes = [];
let directions = [
[1, 0],
[-1, 0],
[0, 1],
[0, -1],
[1, 1],
[1, -1],
[-1, 1],
[-1, -1]
];
let f = "abcdefgh".indexOf(queenSquare[0]);
let r = parseInt(queenSquare[1]);
directions.forEach(function (d) {
let nf = f + d[0],
nr = r + d[1];
while (nf >= 0 && nf <= 7 && nr >= 1 && nr <= 8) {
let sq = "abcdefgh"[nf] + nr;
if (!fenCharAtSquare(fen, sq)) {
escapes.push(sq);
} else {
break;
}
nf += d[0];
nr += d[1];
}
});
return escapes;
}
function getPiecesInRadius(fen, centerSquare, radius, color) {
let pieces = [];
let allPieces = getAllPieces(fen, color);
let cf = "abcdefgh".indexOf(centerSquare[0]);
let cr = parseInt(centerSquare[1]);
allPieces.forEach(function (p) {
let pf = "abcdefgh".indexOf(p.square[0]);
let pr = parseInt(p.square[1]);
let dist = Math.max(Math.abs(cf - pf), Math.abs(cr - pr));
if (dist <= radius) {
pieces.push(p);
}
});
return pieces;
}
// =====================================================
// Section 22: Cache Management
// =====================================================
function clearPremoveCaches() {
if (Engine && Engine._premoveProcessedFens && typeof Engine._premoveProcessedFens.clear === "function") {
Engine._premoveProcessedFens.clear();
}
}
function trimCaches() {
function trimMap(map, maxSize, keepRatio) {
if (!map || typeof map.size !== "number" || map.size <= maxSize) return;
let ratio = typeof keepRatio === "number" ? keepRatio : 0.75;
let keysToDelete = map.size - Math.floor(maxSize * ratio);
let keys = Array.from(map.keys()).slice(0, Math.max(0, keysToDelete));
keys.forEach(function (k) { map.delete(k); });
}
if (Syzygy && Syzygy.cache) {
trimMap(Syzygy.cache, 60, 0.7);
}
}
// =====================================================
// Section 23: Evaluation Parsing
// =====================================================
function normalizeEvaluation(evaluation) {
if (evaluation === null || evaluation === undefined || evaluation === "-" || evaluation === "Error") {
return null;
}
if (typeof evaluation === "object" && evaluation !== null) {
if ("mate" in evaluation && evaluation.mate !== 0) {
return { mate: evaluation.mate };
}
if ("cp" in evaluation) {
return { cp: evaluation.cp };
}
return null;
}
if (typeof evaluation === "string") {
const mateMatch = evaluation.match(/([+-])?M([+-]?\d+)/i);
if (mateMatch) {
const sign = mateMatch[1] === "-" ? -1 : 1;
const moves = Math.abs(parseInt(mateMatch[2], 10));
return { mate: sign * moves };
}
const num = parseFloat(evaluation);
if (!isNaN(num)) {
return { cp: Math.round(num * 100) };
}
return null;
}
if (typeof evaluation === "number") {
return { cp: Math.round(evaluation * 100) };
}
return null;
}
// =====================================================
// Section 24: Premove Chance Calculation
// =====================================================
function getBaseChanceFromParsedEval(parsed) {
if (!parsed) return 0;
if ("mate" in parsed) {
let mateDistance = Math.abs(parsed.mate);
if (parsed.mate > 0) return 2;
if (mateDistance <= 2) return 75;
if (mateDistance <= 4) return 65;
if (mateDistance <= 6) return 55;
return 45;
}
let evalFromSTM = parsed.cp / 100;
let ourEval = -evalFromSTM;
if (ourEval >= 10.0) return 80;
if (ourEval >= 6.0) return 70;
if (ourEval >= 3.5) return 55;
if (ourEval >= 2.0) return 45;
if (ourEval >= 1.0) return 35;
if (ourEval >= 0.3) return 25;
if (ourEval >= 0) return 18;
if (ourEval >= -0.5) return 12;
if (ourEval >= -1.5) return 8;
if (ourEval >= -3.0) return 5;
return 2;
}
function getEvalBasedPremoveChance(evaluation, ourColor) {
if (!State.premoveEnabled) return 0;
let game = getGame();
if (!game || isPlayersTurn(game)) return 0;
let parsed = normalizeEvaluation(evaluation);
if (!parsed) return 0;
return getBaseChanceFromParsedEval(parsed);
}
function parsePVMoves(pv) {
if (!pv || typeof pv !== "string") return [];
let tokenRe = /^[a-h][1-8][a-h][1-8](?:[qrbn])?$/i;
let trimmed = pv.trim();
if (!trimmed) return [];
return trimmed.split(/\s+/).filter(function (t) { return tokenRe.test(t); });
}
function getOurMoveFromPV(pv, ourColor, sideToMove) {
if (!pv) return null;
let moves = parsePVMoves(pv);
if (!moves.length) return null;
let idx = (sideToMove === ourColor) ? 0 : 1;
if (idx >= moves.length) {
State.statusInfo = "[PV] PV too short for premove. Length:" + moves.length + " needed idx:" + idx;
UI.updateStatusInfo();
return null;
}
return moves[idx];
}
// =====================================================
// Section 25: Time and Clock Functions
// =====================================================
function parseTimeString(timeString) {
if (!timeString || typeof timeString !== "string") return null;
let clean = timeString.replace(/[^\d:.]/g, "");
if (!/\d/.test(clean)) return null;
let parts = clean.split(":").map(function (p) { return parseFloat(p); });
if (parts.some(isNaN)) return null;
let total = 0;
if (parts.length === 3) total = parts[0] * 3600 + parts[1] * 60 + parts[2];
else if (parts.length === 2) total = parts[0] * 60 + parts[1];
else if (parts.length === 1) total = parts[0];
return total >= 0 ? total : null;
}
function getClockTimes() {
try {
const clockSelectors = [
".clock-time-monospace[role=\"timer\"]",
".clock-time-monospace",
".clock-component .clock-time-monospace"
];
let allClockElements = [];
for (let si = 0; si < clockSelectors.length; si++) {
const elements = Array.from(document.querySelectorAll(clockSelectors[si]))
.filter(function (el) { return el && el.offsetParent !== null; });
if (elements.length > 0) {
allClockElements = elements;
break;
}
}
if (allClockElements.length === 0) {
return { opponentTime: null, playerTime: null, found: false };
}
const getElementTime = function (el) {
const text = (el.textContent || el.innerText || "").trim();
return parseTimeString(text);
};
let opponentTime = null;
let playerTime = null;
if (allClockElements.length >= 2) {
const sorted = allClockElements
.map(function (el) { return { el: el, rect: el.getBoundingClientRect() }; })
.sort(function (a, b) { return a.rect.top - b.rect.top; })
.map(function (obj) { return obj.el; });
playerTime = getElementTime(sorted[sorted.length - 1]);
opponentTime = getElementTime(sorted[0]);
} else {
playerTime = getElementTime(allClockElements[0]);
}
return { opponentTime: opponentTime, playerTime: playerTime, found: true };
} catch (e) {
return { opponentTime: null, playerTime: null, found: false };
}
}
// =====================================================
// Section 26: Syzygy Tablebase Probe
// =====================================================
let Syzygy = {
cache: new Map(),
inFlight: false,
lastFenHash: "",
lastRequestTs: 0,
THROTTLE_MS: 1200,
MAX_CACHE_SIZE: 120,
MAX_PIECES: 7,
USE_FETCH_FIRST: true,
_countPieces: function (fen) {
if (!fen || typeof fen !== "string") return 99;
let placement = fen.split(" ")[0] || "";
let matches = placement.match(/[pnbrqkPNBRQK]/g);
return matches ? matches.length : 0;
},
_toFenHash: function (fen) {
return hashFen(fen || "");
},
_toApiFenParam: function (fenHash) {
return encodeURIComponent(String(fenHash || "").replace(/\s+/g, "_"));
},
_setStateFromResult: function (fenHash, payload, source) {
State.syzygyLastFen = fenHash;
State.syzygySource = source || "api";
State.syzygyError = "";
State.syzygyMeta = {
category: payload && payload.category ? payload.category : "unknown",
dtz: payload && typeof payload.dtz === "number" ? payload.dtz : null,
dtm: payload && typeof payload.dtm === "number" ? payload.dtm : null,
checkmate: !!(payload && payload.checkmate),
stalemate: !!(payload && payload.stalemate)
};
State.syzygyMoves = Array.isArray(payload && payload.moves) ? payload.moves.slice(0, 10) : [];
State.syzygyStatus = "Ready";
UI.updateSyzygyDisplay();
},
_setUnavailable: function (statusText, errText) {
State.syzygyStatus = statusText || "Unavailable";
State.syzygyError = errText || "";
State.syzygyMoves = [];
State.syzygyMeta = null;
UI.updateSyzygyDisplay();
},
_categoryToEvalCp: function (category) {
let c = String(category || "").toLowerCase();
if (c.includes("win")) return 900;
if (c.includes("cursed")) return 120;
if (c.includes("draw")) return 0;
if (c.includes("blessed")) return -120;
if (c.includes("loss")) return -900;
return 0;
},
tryUseForAnalysis: function (fen) {
if (!State.analysisMode) return false;
let fenHash = this._toFenHash(fen);
if (!fenHash) return false;
if (this._countPieces(fenHash) > this.MAX_PIECES) return false;
if (State.syzygyLastFen !== fenHash) return false;
let moves = Array.isArray(State.syzygyMoves) ? State.syzygyMoves : [];
if (!moves.length) return false;
let maxRows = clamp(State.numberOfMovesToShow || 5, 2, 10);
State.topMoves = [];
State.topMoveInfos = {};
for (let i = 0; i < maxRows; i++) {
let mv = moves[i];
if (!mv || !mv.uci) {
UI.updateMove(i + 1, "...", "-", "eval-equal");
continue;
}
let cat = String(mv.category || "draw").toLowerCase();
let evalCp = this._categoryToEvalCp(cat);
let evalText = (cat === "draw" ? "0.00" : String(mv.category || "TB"));
let evalClass = evalCp > 0 ? "eval-positive" : (evalCp < 0 ? "eval-negative" : "eval-equal");
let uci = String(mv.uci);
State.topMoves[i] = uci;
State.topMoveInfos[i + 1] = {
move: uci,
evalText: evalText,
evalClass: evalClass,
depth: 99,
rawCp: evalCp
};
UI.updateMove(i + 1, uci, evalText, evalClass);
}
let best = moves[0];
if (!best || !best.uci) return false;
let bestUci = String(best.uci);
let bestCat = String(best.category || "draw");
let bestEval = this._categoryToEvalCp(bestCat);
State.analysisEvalText = bestCat === "draw" ? "0.00" : bestCat;
State.analysisPVLine = [bestUci];
State.principalVariation = bestUci;
State.currentEvaluation = bestEval;
State.analysisStableCount = Math.max(State.analysisStableCount || 0, State.analysisMinStableUpdates || 2);
State.analysisLastBestMove = bestUci;
State.analysisGuardStateText = "Syzygy";
State.isAnalysisThinking = false;
State.statusInfo = "Syzygy analysis: " + bestUci + " (" + bestCat + ")";
if (State.analysisAcplFen !== fenHash) {
ACPL.onNewEval(bestEval, null);
State.analysisAcplFen = fenHash;
}
recordAnalysisBestmove(bestUci, State.analysisEvalText, 99, fenHash);
UI.updateStatusInfo();
UI.updateAnalysisBar(bestEval);
if (State.showPVArrows) {
UI.clearPVArrows();
UI.drawPVArrows(State.analysisPVLine, State.analysisPVTurn, true);
}
if (State.showBestmoveArrows) {
UI.drawBestmoveArrows();
}
if (State.highlightEnabled) {
UI.highlightMove(bestUci, State.highlightColor2, true);
}
UI.updatePVDisplay();
return true;
},
maybeProbe: function (fen) {
if (State.gameEnded && !State.analysisMode) return;
if (!fen) return;
let fenHash = this._toFenHash(fen);
if (!fenHash) return;
let pieceCount = this._countPieces(fenHash);
if (pieceCount > this.MAX_PIECES) {
if (State.syzygyStatus !== "Not available (>7 pieces)") {
State.syzygyStatus = "Not available (>7 pieces)";
State.syzygyError = "";
State.syzygyMoves = [];
State.syzygyMeta = null;
UI.updateSyzygyDisplay();
}
return;
}
let cached = this.cache.get(fenHash);
if (cached && (Date.now() - cached.ts) < 300000) {
this._setStateFromResult(fenHash, cached.payload, "cache");
this.tryUseForAnalysis(fenHash);
return;
}
let now = Date.now();
if (this.inFlight && this.lastFenHash === fenHash) return;
if ((now - this.lastRequestTs) < this.THROTTLE_MS && this.lastFenHash === fenHash) return;
this.lastRequestTs = now;
this.lastFenHash = fenHash;
this.inFlight = true;
State.syzygyStatus = "Loading...";
State.syzygyError = "";
UI.updateSyzygyDisplay();
let url = "https://tablebase.lichess.ovh/standard?fen=" + this._toApiFenParam(fenHash);
let self = this;
let handlePayload = function (payload, source) {
self.cache.set(fenHash, { payload: payload, ts: Date.now() });
if (self.cache.size > self.MAX_CACHE_SIZE) {
let keys = Array.from(self.cache.keys()).slice(0, self.cache.size - self.MAX_CACHE_SIZE);
keys.forEach(function (k) { self.cache.delete(k); });
}
self._setStateFromResult(fenHash, payload, source);
self.tryUseForAnalysis(fenHash);
};
if (typeof fetch !== "function") {
self.inFlight = false;
self._setUnavailable("Unavailable", "Fetch not supported");
return;
}
fetch(url, {
method: "GET",
mode: "cors",
credentials: "omit",
cache: "no-store"
}).then(function (resp) {
if (!resp || !resp.ok) throw new Error("HTTP " + ((resp && resp.status) || 0));
return resp.json();
}).then(function (payload) {
self.inFlight = false;
handlePayload(payload || {}, "api-fetch");
}).catch(function (fetchErr) {
self.inFlight = false;
self._setUnavailable("Unavailable", (fetchErr && fetchErr.message) ? fetchErr.message : "Fetch failed");
});
},
clear: function () {
this.cache.clear();
this.inFlight = false;
this.lastFenHash = "";
this.lastRequestTs = 0;
State.syzygyStatus = "Idle";
State.syzygyLastFen = "";
State.syzygySource = "";
State.syzygyMoves = [];
State.syzygyMeta = null;
State.syzygyError = "";
}
};
// =====================================================
// Section 27: Advanced Time Management
// =====================================================
let TimeManager = {
lastMoveTime: 0,
moveTimes: [],
isTimePressure: false,
calculateHumanizedDelay: function () {
if (State.clockSync) return this._calculateClockSyncDelay();
let range = this._getValidatedDelayRange();
let minD = range.min;
let maxD = range.max;
let minMs = minD * 1000;
let maxMs = maxD * 1000;
let baseDelay = (Math.random() * (maxD - minD) + minD) * 1000;
this.isTimePressure = false;
if (CONFIG.STEALTH.RANDOMIZE_DELAYS) {
let complexity = this._estimatePositionComplexity();
let complexityBonus = complexity * 0.15;
baseDelay *= (1 + complexityBonus);
if (State.moveNumber <= 10) baseDelay *= 0.85;
}
baseDelay = Math.max(minMs, Math.min(maxMs, baseDelay));
let jitter = 1 + (Math.random() - 0.5) * 0.1;
baseDelay *= jitter;
this.moveTimes.push(baseDelay);
if (this.moveTimes.length > 10) this.moveTimes.shift();
return Math.max(100, Math.round(baseDelay));
},
_lastClockData: null,
_lastClockTs: 0,
_getCachedClockData: function () {
let now = Date.now();
if (this._lastClockData && now - this._lastClockTs < 500) return this._lastClockData;
this._lastClockData = getClockTimes();
this._lastClockTs = now;
return this._lastClockData;
},
_detectIncrement: function () {
let clockData = this._getCachedClockData();
if (!clockData || !clockData.found) return 0;
try {
let selectors = [
"[data-cy='clock-component']",
".clock-component",
".board-layout-top .clock-component",
".board-layout-bottom .clock-component"
];
for (let i = 0; i < selectors.length; i++) {
let el = document.querySelector(selectors[i]);
if (!el) continue;
let text = (el.textContent || el.innerText || "").trim();
let incMatch = text.match(/\+(\d+)/);
if (incMatch) return parseInt(incMatch[1], 10);
}
let gameInfo = document.querySelector("[data-cy='game-info-time']");
if (gameInfo) {
let txt = (gameInfo.textContent || "").trim();
let m = txt.match(/\|?\s*\+(\d+)/);
if (m) return parseInt(m[1], 10);
}
} catch (e) { }
return 0;
},
_calculateClockSyncDelay: function () {
let clockData = this._getCachedClockData();
if (!clockData || !clockData.found || clockData.playerTime === null) {
return this._calculateRandomDelay();
}
let myTimeSec = clockData.playerTime;
if (myTimeSec <= 0) {
this.isTimePressure = true;
return 100;
}
let quickThreshold = State.clockSyncLowTimeQuickSec;
let quickDelayMs = State.clockSyncQuickDelayMs || 300;
if (myTimeSec <= quickThreshold) {
let ratio = myTimeSec / quickThreshold;
let urgencyFactor;
if (myTimeSec <= 3) urgencyFactor = 0.5;
else if (myTimeSec <= 10) urgencyFactor = Math.max(0.5, ratio * 0.8);
else urgencyFactor = Math.max(0.6, ratio);
let adjustedDelay = Math.round(quickDelayMs * urgencyFactor);
adjustedDelay = Math.max(200, Math.min(adjustedDelay, quickDelayMs));
let jitter = 1 + (Math.random() - 0.5) * 0.15;
adjustedDelay = Math.round(adjustedDelay * jitter);
log("[TimeManager] LOW TIME! " + myTimeSec.toFixed(1) + "s → delay: " + adjustedDelay + "ms (factor:" + urgencyFactor.toFixed(2) + ")");
this.isTimePressure = true;
return Math.max(150, adjustedDelay);
}
this.isTimePressure = false;
let incrementSec = this._detectIncrement();
let myTimeMs = myTimeSec * 1000;
let incrementMs = incrementSec * 1000;
let moveNum = State.moveNumber;
let estimatedTotalMoves = this._estimateGameLength(clockData);
let remainingMoves = Math.max(5, estimatedTotalMoves - moveNum);
let phaseMultiplier = 1.0;
if (moveNum <= 10) phaseMultiplier = 0.6;
else if (moveNum <= 25) phaseMultiplier = 1.2;
else if (moveNum <= 40) phaseMultiplier = 1.0;
else phaseMultiplier = 0.7;
let timeForThisMove = (myTimeMs / remainingMoves) * phaseMultiplier;
timeForThisMove += (incrementMs * 0.4);
let minTime = State.clockSyncMinDelay * 1000;
let maxTime = State.clockSyncMaxDelay * 1000;
if (minTime > maxTime) { let tmp = minTime; minTime = maxTime; maxTime = tmp; }
let finalDelay = Math.min(Math.max(timeForThisMove, minTime), maxTime);
let jitter = 1 + (Math.random() - 0.5) * 0.2;
finalDelay = Math.round(finalDelay * jitter);
log("[TimeManager] Clock sync: " + myTimeSec.toFixed(1) + "s left, inc+" + incrementSec + "s, move#" + moveNum + ", delay: " + finalDelay + "ms");
return Math.max(150, finalDelay);
},
_estimatePositionComplexity: function () {
let fen = getAccurateFen();
if (!fen) return 1.0;
let pieceCount = (fen.match(/[pnbrqkPNBRQK]/g) || []).length;
let baseComplexity = pieceCount / 32;
let kingExposure = this._estimateKingExposure(fen);
return Math.min(2.0, baseComplexity + kingExposure);
},
_estimateKingExposure: function (fen) {
let parts = fen.split(" ");
let castling = parts[2] || "-";
return castling === "-" ? 0.3 : 0.1;
},
_estimateGameLength: function (clockData) {
let tc = this._detectTimeControl(clockData);
if (tc === "bullet") return 40;
if (tc === "blitz") return 50;
if (tc === "rapid") return 60;
return 70;
},
_detectTimeControl: function (clockData) {
let data = clockData || this._getCachedClockData();
if (!data || !data.found) return "rapid";
let remainingTime = data.playerTime || 0;
let moveNum = State.moveNumber || 1;
let incrementSec = this._detectIncrement();
let avgMoveTime = moveNum > 1 ? 3 + incrementSec : 5;
let estimatedInitialTime = remainingTime + (moveNum * avgMoveTime);
if (estimatedInitialTime <= 120) return "bullet";
if (estimatedInitialTime <= 420) return "blitz";
if (estimatedInitialTime <= 1500) return "rapid";
return "classical";
},
_getValidatedDelayRange: function () {
let minD = Number(State.minDelay);
let maxD = Number(State.maxDelay);
if (!isFinite(minD) || minD <= 0) minD = 0.1;
if (!isFinite(maxD) || maxD <= 0) maxD = minD;
if (minD > maxD) {
let tmp = minD;
minD = maxD;
maxD = tmp;
}
return {
min: minD,
max: maxD
};
},
_calculateRandomDelay: function () {
let range = this._getValidatedDelayRange();
let minD = range.min;
let maxD = range.max;
return (Math.random() * (maxD - minD) + minD) * 1000;
}
};
function getCalculatedDelay() {
return TimeManager.calculateHumanizedDelay();
}
function cancelPendingMove() {
if (pendingMoveTimeoutId) {
clearTimeout(pendingMoveTimeoutId);
pendingMoveTimeoutId = null;
}
State.moveExecutionInProgress = false;
}
function cancelModePendingTimers(reason) {
cancelPendingMove();
if (Engine && Engine._premoveTimeoutId) {
clearTimeout(Engine._premoveTimeoutId);
Engine._premoveTimeoutId = null;
}
if (Engine) {
Engine._premoveProcessing = false;
Engine._premoveEngineBusy = false;
}
State.premoveAnalysisInProgress = false;
State.currentDelayMs = 0;
}
// =====================================================
// Section 28: Opponent Rating and Depth Adaptation
// =====================================================
function extractOpponentRating() {
try {
let selectors = [
"#board-layout-player-top .rating",
"#board-layout-player-bottom .rating",
"#board-layout-player-top [class*='rating']",
"#board-layout-player-bottom [class*='rating']",
".user-tagline-rating",
".player-component .rating",
".player-top .rating",
".player-bottom .rating",
".board-layout-player-top .rating",
".board-layout-player-bottom .rating"
];
for (let i = 0; i < selectors.length; i++) {
let el = document.querySelector(selectors[i]);
if (el && el.textContent) {
let rating = parseInt(el.textContent.replace(/\D/g, ""), 10);
if (!isNaN(rating) && rating > 100) return rating;
}
}
let boardArea = document.querySelector("#board-layout-chessboard") ||
document.querySelector("chess-board") ||
document.querySelector(".board");
if (boardArea) {
let allText = boardArea.parentElement.textContent || "";
let ratingMatches = allText.match(/\d{3,4}\s*[♙♘♗♖♕♔⚫⚪]/g);
if (ratingMatches && ratingMatches.length >= 2) {
return parseInt(ratingMatches[0].replace(/\D/g, ""), 10);
}
}
let game = getGame();
if (game && game.players) {
let myColor = getPlayingAs();
for (let color in game.players) {
if (color !== myColor && game.players[color].rating) {
return parseInt(game.players[color].rating, 10);
}
}
}
} catch (e) {
console.error("[ChessAssistant] Error extracting rating:", e);
}
return null;
}
function mapRatingToDepth(r) {
if (!r || r < 600) return 1;
if (r < 900) return 3;
if (r < 1100) return 5;
if (r < 1300) return 7;
if (r < 1500) return 9;
if (r < 1700) return 12;
if (r < 1900) return 15;
if (r < 2100) return 18;
if (r < 2300) return 22;
if (r < 2500) return 24;
return Math.min(26, CONFIG.MAX_DEPTH);
}
function mapRatingToElo(r) {
if (!r || !Number.isFinite(r)) return 1600;
let mapped = clamp(Math.round(r / 10) * 10, 300, 3200);
return mapped;
}
function applyAutoDepthFromOpponent() {
if (!State.autoDepthAdapt) {
return;
}
let opp = extractOpponentRating();
if (!opp) {
console.log("[ChessAssistant] Could not extract opponent rating");
State.statusInfo = "Auto Depth: No rating found";
UI.updateStatusInfo();
return;
}
let newDepth = mapRatingToDepth(opp);
let newElo = mapRatingToElo(opp);
console.log("[ChessAssistant] Opponent rating:", opp, "-> Depth:", newDepth, "ELO:", newElo);
saveSetting("customDepth", newDepth);
saveSetting("eloRating", newElo);
if (State.evaluationMode === "human") {
Engine.setElo(newElo);
}
_updateDepthSliderUI(newDepth, newElo, opp);
State.statusInfo = "Auto Adapt: " + opp + " -> Depth " + newDepth + " | ELO " + newElo;
UI.updateStatusInfo();
}
function _updateDepthSliderUI(depth, elo, rating) {
let attempts = 0;
let maxAttempts = 5;
function tryUpdate() {
let sld = $("#sld-depth");
let disp = $("#depth-display");
let eloSlider = $("#sld-elo");
let eloDisplay = $("#elo-display");
if (sld && disp) {
sld.value = depth;
disp.textContent = depth;
let event = new Event('input', { bubbles: true });
sld.dispatchEvent(event);
let changeEvent = new Event('change', { bubbles: true });
sld.dispatchEvent(changeEvent);
if (eloSlider && eloDisplay && Number.isFinite(elo)) {
eloSlider.value = elo;
eloDisplay.textContent = elo;
eloSlider.dispatchEvent(new Event("input", { bubbles: true }));
}
console.log("[ChessAssistant] Auto-adapt UI updated depth:", depth, "elo:", elo, "opp:", rating);
return true;
}
attempts++;
if (attempts < maxAttempts) {
scheduleManagedTimeout(tryUpdate, 100);
} else {
console.warn("[ChessAssistant] Could not find depth slider after", maxAttempts, "attempts");
}
return false;
}
tryUpdate();
}
// =====================================================
// Section 29: Engine Management
// =====================================================
let Engine = {
main: null,
mainBlobURL: null,
_ready: false,
analysis: null,
analysisBlobURL: null,
premove: null,
premoveBlobURL: null,
_activeBlobURLs: new Set(),
_premoveEngineBusy: false,
_premoveProcessedFens: new Set(),
_premoveProcessing: false,
_premoveLastFen: null,
_premoveTimeoutId: null,
_premoveCandidates: Object.create(null),
_premoveAttemptedFens: new Set(),
_premoveLastActivityTs: 0,
_mainLastActivityTs: 0,
_analysisLastActivityTs: 0,
init: function () {
let self = this;
let src = "";
try {
src = GM_getResourceText("stockfishjs");
} catch (e) { }
if (!src || src.length < 1000) {
State.statusInfo = "GM_getResourceText unavailable, trying manual load...";
UI.updateStatusInfo();
return loadStockfishManually().then(function (loaded) {
if (!loaded) {
err("All Stockfish load methods failed");
return false;
}
return self.loadMainEngine();
});
}
return self.loadMainEngine();
},
_registerBlobURL: function (url) {
if (!url) return;
try {
this._activeBlobURLs.add(url);
} catch (e) { }
},
_revokeBlobURL: function (url) {
if (!url) return;
try {
URL.revokeObjectURL(url);
} catch (e) { }
try {
this._activeBlobURLs.delete(url);
} catch (e) { }
},
_revokeAllActiveBlobURLs: function () {
try {
this._activeBlobURLs.forEach(function (url) {
try { URL.revokeObjectURL(url); } catch (e) { }
});
} catch (e) { }
try {
this._activeBlobURLs.clear();
} catch (e) { }
},
_createWorker: function (existingBlobURL) {
if (existingBlobURL) {
this._revokeBlobURL(existingBlobURL);
}
let src = "";
try {
src = GM_getResourceText("stockfishjs");
} catch (e) { }
if (!src || src.length < 1000) src = EngineLoader.stockfishSourceCode;
if (!src || src.length < 1000) {
err("No Stockfish source available");
return {
worker: null,
blobURL: null
};
}
try {
let blob = new Blob([src], {
type: "application/javascript"
});
let blobURL = URL.createObjectURL(blob);
this._registerBlobURL(blobURL);
let worker = new Worker(blobURL);
worker._blobURL = blobURL;
return {
worker: worker,
blobURL: blobURL
};
} catch (e) {
err("Worker creation failed:", e);
return {
worker: null,
blobURL: null
};
}
},
_waitForSignal: function (engineWorker, signal, timeout) {
return new Promise(function (resolve, reject) {
let timer;
let handler = function (e) {
if (typeof e.data === "string" && e.data.includes(signal)) {
clearTimeout(timer);
engineWorker.removeEventListener("message", handler);
resolve();
}
};
engineWorker.addEventListener("message", handler);
timer = setTimeout(function () {
clearTimeout(timer);
engineWorker.removeEventListener("message", handler);
reject(new Error("Timeout waiting for: " + signal));
}, timeout);
});
},
loadMainEngine: function () {
let self = this;
try {
if (self.main) {
self.main.terminate();
self.main = null;
}
let result = self._createWorker(self.mainBlobURL);
if (!result.worker) return Promise.resolve(false);
self.main = result.worker;
self.mainBlobURL = result.blobURL;
self._ready = false;
self.main.onmessage = function (e) {
self._onMainMessage(e.data);
};
self.main.onerror = function () {
self._ready = false;
if (self.main && self.main._blobURL) {
self._revokeBlobURL(self.main._blobURL);
}
};
return new Promise(function (resolve) {
let attempt = function (n) {
if (n > 3) {
resolve(false);
return;
}
self.main.postMessage("uci");
self._waitForSignal(self.main, "uciok", 8000).then(function () {
self._configureMainEngine();
self.main.postMessage("isready");
return self._waitForSignal(self.main, "readyok", 5000);
}).then(function () {
self._ready = true;
self._mainLastActivityTs = Date.now();
let led = $("#engine-status-led");
if (led) led.classList.add("active");
resolve(true);
}).catch(function () {
scheduleManagedTimeout(function () {
attempt(n + 1);
}, 1000 * n);
});
};
attempt(1);
});
} catch (e) {
err("Main engine init failed:", e);
return Promise.resolve(false);
}
},
_configureMainEngine: function () {
let mpv = clamp(State.numberOfMovesToShow || 5, 2, 10);
this.main.postMessage("setoption name MultiPV value " + mpv);
this.main.postMessage("setoption name Skill Level value " + (State.skillLevel !== undefined ? State.skillLevel : 20));
if (State.evaluationMode === "human") {
this.main.postMessage("setoption name UCI_LimitStrength value true");
this.main.postMessage("setoption name UCI_Elo value " + State.eloRating);
} else {
this.main.postMessage("setoption name UCI_LimitStrength value false");
}
this.main.postMessage("ucinewgame");
},
go: function (fen, depth) {
if (!this.main || !this._ready) return;
if (State.analysisMode) {
State.statusInfo = "Main engine blocked: Analysis mode active";
UI.updateStatusInfo();
return;
}
State.isThinking = true;
State.statusInfo = "Analyzing...";
UI.updateStatusInfo();
State.topMoves = [];
State.topMoveInfos = {};
State.topMovesFen = fen;
State.mainBestHistory = [];
State.mainPVLine = [];
State.mainPVTurn = getCurrentTurn(fen);
State._mainDepthByPv = {};
let maxRows = clamp(State.numberOfMovesToShow || 5, 2, 10);
for (let i = 1; i <= maxRows; i++) {
UI.updateMove(i, "...", "0.00", "eval-equal");
}
UI.clearBestmoveArrows();
UI.clearHighlights();
this.main.postMessage("stop");
this.main.postMessage("position fen " + fen);
this._mainLastActivityTs = Date.now();
if (State.evaluationMode === "human") {
let level = ELO_LEVELS[State.humanLevel] || ELO_LEVELS.intermediate;
let ms = Math.floor((level.moveTime.min + Math.random() * (level.moveTime.max - level.moveTime.min)) * 1000);
this.main.postMessage("go movetime " + ms);
} else {
this.main.postMessage("go depth " + depth);
}
UI.updateStatusInfo();
},
stop: function () {
if (this.main) this.main.postMessage("stop");
State.isThinking = false;
State.statusInfo = "Ready";
UI.updateStatusInfo();
},
setElo: function (elo) {
if (!this.main) return;
this.main.postMessage("setoption name UCI_LimitStrength value true");
this.main.postMessage("setoption name UCI_Elo value " + elo);
this.main.postMessage("isready");
},
setFullStrength: function () {
if (!this.main) return;
this.main.postMessage("setoption name UCI_LimitStrength value false");
this.main.postMessage("setoption name Skill Level value " + (State.skillLevel !== undefined ? State.skillLevel : 20));
this.main.postMessage("isready");
},
setSkillLevel: function (level) {
if (!this.main) return;
let val = clamp(level, 0, 20);
this.main.postMessage("setoption name Skill Level value " + val);
this.main.postMessage("isready");
log("[Engine] Skill Level set to " + val);
},
_onMainMessage: function (data) {
if (typeof data !== "string") return;
this._mainLastActivityTs = Date.now();
if (State.analysisMode) {
if (data.indexOf("bestmove") === 0) State.isThinking = false;
return;
}
if (data.indexOf("info") === 0 && data.includes(" pv ")) {
this._parseMainInfo(data);
} else if (data.indexOf("bestmove") === 0) {
this._handleMainBestMove(data);
}
},
_parseMainInfo: function (data) {
let tokens = data.split(" ");
let get = function (key) {
let i = tokens.indexOf(key);
return (i !== -1 && i + 1 < tokens.length) ? tokens[i + 1] : null;
};
let multipv = parseInt(get("multipv")) || 1;
let depth = parseInt(get("depth")) || 0;
if (!State._mainDepthByPv) State._mainDepthByPv = {};
let lastDepth = State._mainDepthByPv[multipv] || 0;
if (depth < lastDepth) return;
State._mainDepthByPv[multipv] = depth;
let pvIdx = tokens.indexOf("pv");
if (pvIdx === -1) return;
let bestMove = tokens[pvIdx + 1];
let pvMoves = tokens.slice(pvIdx + 1).filter(function (m) {
return /^[a-h][1-8][a-h][1-8](?:[qrbn])?$/i.test(m);
});
let scoreIdx = tokens.indexOf("score");
if (scoreIdx === -1) return;
let scoreType = tokens[scoreIdx + 1];
let scoreValue = parseInt(tokens[scoreIdx + 2]) || 0;
let maxTrackedMoves = clamp(State.numberOfMovesToShow || 5, 2, 10);
if (multipv >= 1 && multipv <= maxTrackedMoves) State.topMoves[multipv - 1] = bestMove;
let evalText = "0.00";
let evalClass = "eval-equal";
let rawCp = 0;
let mateVal = null;
if (scoreType === "cp") {
rawCp = scoreValue;
evalText = (rawCp >= 0 ? "+" : "") + (rawCp / 100).toFixed(2);
evalClass = rawCp > 30 ? "eval-positive" : rawCp < -30 ? "eval-negative" : "eval-equal";
} else if (scoreType === "mate") {
mateVal = scoreValue;
rawCp = scoreValue > 0 ? CONFIG.MATE_VALUE : -CONFIG.MATE_VALUE;
evalText = (scoreValue > 0 ? "M+" : "M") + scoreValue;
evalClass = scoreValue > 0 ? "eval-positive" : "eval-negative";
}
if (multipv >= 1 && multipv <= maxTrackedMoves) {
State.topMoveInfos[multipv] = {
move: bestMove,
evalText: evalText,
evalClass: evalClass,
depth: depth,
rawCp: rawCp
};
UI.updateMove(multipv, bestMove, evalText, evalClass);
if (State.showBestmoveArrows && !State.analysisMode) {
UI.drawBestmoveArrows();
}
}
if (multipv === 1) {
let oldPV = State.mainPVLine.join(" ");
let newPV = pvMoves.join(" ");
State.mainBestHistory.push({ move: bestMove, depth: depth, ts: Date.now() });
if (State.mainBestHistory.length > 8) {
State.mainBestHistory = State.mainBestHistory.slice(-8);
}
State.mainPVLine = pvMoves;
State.principalVariation = newPV;
State.lastTopMove1 = bestMove;
State.lastEvalText1 = evalText;
State.lastEvalClass1 = evalClass;
State.currentEvaluation = rawCp;
State._lastScoreInfo = {
type: scoreType,
value: scoreValue,
display: evalText
};
if (depth >= 8) checkAutoResign(scoreType, scoreValue);
UI.updateMove(1, bestMove, evalText, evalClass);
UI.updateEvalBar(rawCp, mateVal, depth);
ACPL.onNewEval(rawCp, mateVal);
if (State.premoveEnabled) {
let game = getGame();
UI.updatePremoveChanceDisplay(game, rawCp, evalText, bestMove, 1);
}
if (!State.analysisMode && State.showPVArrows) {
if (oldPV !== newPV || pvMoves[0] !== State.lastRenderedMainPV.split(" ")[0]) {
UI._removePVArrowsByType(false);
UI.drawPVArrows(pvMoves, State.mainPVTurn, false);
}
}
UI.updatePVDisplay();
}
},
_handleMainBestMove: function (data) {
let tokens = data.split(" ");
let finalMove = tokens[1];
if (!finalMove || finalMove === "(none)") {
State.isThinking = false;
return;
}
let game = getGame();
if (!isPlayersTurn(game)) {
State.isThinking = false;
State.statusInfo = "Waiting for opponent";
UI.updateStatusInfo();
return;
}
let lastScore = State._lastScoreInfo;
if (lastScore) checkAutoResign(lastScore.type, lastScore.value);
if (State.analysisMode) {
State.isThinking = false;
return;
}
finalMove = getMainConsensusMove(finalMove);
if (State.evaluationMode === "human" && State.topMoves.length >= 2 && State.topMoves[1]) {
let currentFen = getAccurateFen();
if (State.topMovesFen === currentFen) {
let level = ELO_LEVELS[State.humanLevel] || ELO_LEVELS.intermediate;
let uniqueMoves = State.topMoves.filter(function (mv, idx, arr) {
return !!mv && arr.indexOf(mv) === idx;
});
let ourColor = getPlayingAs();
let humanFallback = pickHumanFallbackMove(currentFen, uniqueMoves, level, ourColor);
if (humanFallback && humanFallback !== finalMove) {
finalMove = humanFallback;
}
}
}
let fen = getAccurateFen();
let currentPV = State.mainPVLine;
let needsRedraw = false;
if (currentPV.length === 0) {
State.mainPVLine = [finalMove];
State.principalVariation = finalMove;
needsRedraw = true;
} else if (currentPV[0] !== finalMove) {
State.statusInfo = "Bestmove changed: " + currentPV[0] + " -> " + finalMove;
UI.updateStatusInfo();
State.mainPVLine = [finalMove].concat(currentPV.slice(1));
State.principalVariation = State.mainPVLine.join(" ");
needsRedraw = true;
}
State.lastRenderedMainPV = "";
State.lastMainPVDrawTime = 0;
if (!State.analysisMode && State.showPVArrows && needsRedraw) {
UI._removePVArrowsByType(false);
UI.drawPVArrows(State.mainPVLine, State.mainPVTurn, false);
}
UI.updatePVDisplay();
MoveExecutor.recordMove(finalMove);
State.isThinking = false;
if (State.autoMovePiece) {
executeAction(finalMove, fen);
} else {
State.statusInfo = "Best: " + finalMove + " (manual mode)";
UI.updateStatusInfo();
}
},
loadAnalysisEngine: function () {
let self = this;
try {
if (self.analysis) {
self.analysis.terminate();
self.analysis = null;
}
let result = self._createWorker(self.analysisBlobURL);
if (!result.worker) return false;
self.analysis = result.worker;
self.analysisBlobURL = result.blobURL;
self._analysisLastActivityTs = Date.now();
self.analysis.onmessage = function (e) {
self._onAnalysisMessage(e.data);
};
self.analysis.onerror = function () {
if (self.analysis && self.analysis._blobURL) {
self._revokeBlobURL(self.analysis._blobURL);
}
};
scheduleManagedTimeout(function () {
self.analysis.postMessage("uci");
self.analysis.postMessage("setoption name MultiPV value " + clamp(State.numberOfMovesToShow || 5, 2, 10));
self.analysis.postMessage("ucinewgame");
self.analysis.postMessage("isready");
}, 100);
State.statusInfo = "Analysis engine loaded";
UI.updateStatusInfo();
return true;
} catch (e) {
err("Analysis engine load failed:", e);
return false;
}
},
_onAnalysisMessage: function (data) {
if (typeof data !== "string") return;
this._analysisLastActivityTs = Date.now();
if (!State.analysisMode) return;
let currentFen = getAccurateFen();
if (!currentFen) return;
if (State._lastAnalysisFen && normalizeFen(currentFen) !== normalizeFen(State._lastAnalysisFen)) {
State.statusInfo = "FEN changed during analysis, skipping update";
UI.updateStatusInfo();
return;
}
if (data.indexOf("info") === 0 && data.includes(" pv ")) {
let tokens = data.split(" ");
let pvIdx = tokens.indexOf("pv");
let scoreIdx = tokens.indexOf("score");
let depthIdx = tokens.indexOf("depth");
if (pvIdx === -1 || scoreIdx === -1) return;
let multipvIdx = tokens.indexOf("multipv");
let multipv = multipvIdx !== -1 ? (parseInt(tokens[multipvIdx + 1], 10) || 1) : 1;
let maxTrackedMoves = clamp(State.numberOfMovesToShow || 5, 2, 10);
let currentDepth = depthIdx !== -1 ? (parseInt(tokens[depthIdx + 1]) || 0) : 0;
if (!State._analysisDepthByPv) State._analysisDepthByPv = {};
let lastDepth = State._analysisDepthByPv[multipv] || 0;
if (currentDepth < lastDepth) return;
State._analysisDepthByPv[multipv] = currentDepth;
let scoreType = tokens[scoreIdx + 1];
let scoreValue = parseInt(tokens[scoreIdx + 2]) || 0;
let rawCp = scoreType === "cp" ? scoreValue :
scoreType === "mate" ? (scoreValue > 0 ? CONFIG.MATE_VALUE : -CONFIG.MATE_VALUE) : 0;
let pvMoves = tokens.slice(pvIdx + 1).filter(function (m) {
return /^[a-h][1-8][a-h][1-8](?:[qrbn])?$/i.test(m);
});
if (pvMoves.length === 0) return;
let bestMove = pvMoves[0];
let evalText = "0.00";
let evalClass = "eval-equal";
if (scoreType === "cp") {
evalText = (rawCp >= 0 ? "+" : "") + (rawCp / 100).toFixed(2);
evalClass = rawCp > 30 ? "eval-positive" : rawCp < -30 ? "eval-negative" : "eval-equal";
} else if (scoreType === "mate") {
evalText = (scoreValue > 0 ? "M+" : "M") + scoreValue;
evalClass = scoreValue > 0 ? "eval-positive" : "eval-negative";
}
State.analysisEvalText = evalText;
if (multipv >= 1 && multipv <= maxTrackedMoves) {
State.topMoves[multipv - 1] = bestMove;
State.topMoveInfos[multipv] = {
move: bestMove,
evalText: evalText,
evalClass: evalClass,
depth: currentDepth,
rawCp: rawCp
};
UI.updateMove(multipv, bestMove, evalText, evalClass);
}
if (multipv === 1) {
if (bestMove === State.analysisLastBestMove) {
State.analysisStableCount++;
} else {
State.analysisLastBestMove = bestMove;
if (isAnalysisAutoPlayEnabled() && State.analysisStableCount > 1) {
State.analysisStableCount = Math.max(1, State.analysisStableCount - 1);
} else {
State.analysisStableCount = 1;
}
}
if (currentDepth >= (State._lastAnalysisDepth || 0)) {
State.analysisPrevEvalCp = State.analysisLastEvalCp;
State.analysisLastEvalCp = rawCp;
}
State.analysisGuardStateText = "Monitoring";
UI.updateAnalysisMonitorDisplay();
State.statusInfo = "Analysis D" + currentDepth + " | " + bestMove + " | PV: " + pvMoves.slice(0, 3).join(" ");
UI.updateStatusInfo();
if (currentDepth >= State._lastAnalysisDepth) {
State._lastAnalysisDepth = currentDepth;
State._lastAnalysisBestPV = pvMoves.slice();
State._lastAnalysisBestMove = bestMove;
}
State.analysisPVLine = pvMoves;
State.principalVariation = pvMoves.join(" ");
State.currentEvaluation = rawCp;
if (State.analysisAcplFen !== currentFen) {
let mateVal = scoreType === "mate" ? scoreValue : null;
ACPL.onNewEval(rawCp, mateVal);
State.analysisAcplFen = currentFen;
}
UI.updateAnalysisBar(rawCp);
UI.clearAll();
if (State.highlightEnabled) UI.highlightMove(bestMove, State.highlightColor2, true);
if (State.showPVArrows) UI.drawPVArrows(pvMoves, State.analysisPVTurn, true);
if (State.showBestmoveArrows) UI.drawBestmoveArrows();
UI.updatePVDisplay();
} else if (State.showBestmoveArrows) {
UI.drawBestmoveArrows();
}
}
if (data.indexOf("bestmove") === 0 && State.analysisMode) {
let bmTokens = data.split(" ");
let finalBestMove = bmTokens[1];
if (finalBestMove && finalBestMove !== "(none)") {
State.statusInfo = "Analysis: " + finalBestMove;
UI.updateStatusInfo();
if (State._lastAnalysisBestPV.length > 0 && State._lastAnalysisBestPV[0] === finalBestMove) {
State.analysisPVLine = State._lastAnalysisBestPV.slice();
} else {
State.analysisPVLine = [finalBestMove];
}
State.principalVariation = State.analysisPVLine.join(" ");
State.analysisPVTurn = getCurrentTurn(State._lastAnalysisFen || getAccurateFen());
recordAnalysisBestmove(
finalBestMove,
State.analysisEvalText || State.lastEvalText1 || "0.00",
State._lastAnalysisDepth || State.customDepth,
State._lastAnalysisFen || getAccurateFen()
);
UI.clearAll();
if (State.showPVArrows) UI.drawPVArrows(State.analysisPVLine, State.analysisPVTurn, true);
if (State.showBestmoveArrows) UI.drawBestmoveArrows();
if (State.highlightEnabled) UI.highlightMove(finalBestMove, State.highlightColor2, true);
UI.updatePVDisplay();
if (shouldAutoAnalysisMove(finalBestMove)) {
let delay = getCalculatedDelay();
State.currentDelayMs = delay;
let moveToPlay = finalBestMove;
let fenAtDecision = getAccurateFen();
scheduleManagedTimeout(function () {
if (!State.analysisMode || State.autoAnalysisColor === "none") return;
let currentFen = getAccurateFen();
if (!currentFen) return;
let turn = getCurrentTurn(currentFen);
let stillMyTurn = (State.autoAnalysisColor === "white" && turn === "w") ||
(State.autoAnalysisColor === "black" && turn === "b");
if (!stillMyTurn) return;
if (currentFen !== fenAtDecision) return;
State.analysisPrevEvalCp = null;
State.analysisLastEvalCp = null;
State._analysisAutoPlayApproved = false;
State._analysisAutoPlayMove = null;
MoveExecutor.movePiece(
moveToPlay.substring(0, 2),
moveToPlay.substring(2, 4),
moveToPlay.length > 4 ? moveToPlay.substring(4) : null
).then(function (moved) {
if (!moved) log("[Analysis] Auto-play move failed, will retry next cycle");
}).catch(function (e) {
warn("[Analysis] Auto-play error:", e);
});
}, delay);
}
}
State._lastAnalysisDepth = 0;
State._lastAnalysisBestPV = [];
State._lastAnalysisBestMove = null;
State.isAnalysisThinking = false;
State.statusInfo = "Ready";
UI.updateStatusInfo();
UI.updateAnalysisMonitorDisplay();
}
},
loadPremoveEngine: function () {
let self = this;
try {
if (self.premove) {
self.premove.terminate();
self.premove = null;
}
self._premoveProcessedFens.clear();
self._premoveAttemptedFens.clear();
self._premoveCandidates = Object.create(null);
self._premoveProcessing = false;
self._premoveEngineBusy = false;
self._premoveLastFen = null;
self._premoveLastActivityTs = Date.now();
if (self._premoveTimeoutId) {
clearTimeout(self._premoveTimeoutId);
self._premoveTimeoutId = null;
}
let result = self._createWorker(self.premoveBlobURL);
if (!result.worker) {
err("Failed to create premove worker");
return false;
}
self.premove = result.worker;
self.premoveBlobURL = result.blobURL;
self.premove.onmessage = function (e) {
self._onPremoveMessage(e.data);
};
self.premove.onerror = function (workerErr) {
err("Premove engine error:", workerErr);
self._premoveEngineBusy = false;
self._premoveProcessing = false;
if (self.premove && self.premove._blobURL) {
self._revokeBlobURL(self.premove._blobURL);
}
};
scheduleManagedTimeout(function () {
if (!self.premove) return;
self.premove.postMessage("uci");
self.premove.postMessage("setoption name MultiPV value 2");
self.premove.postMessage("ucinewgame");
self.premove.postMessage("isready");
}, 100);
log("[Engine] Premove engine loaded successfully");
return true;
} catch (e) {
err("Premove engine load failed:", e);
return false;
}
},
_onPremoveMessage: async function (data) {
this._premoveLastActivityTs = Date.now();
if (typeof data !== "string") return;
if (State.analysisMode) return;
const currentFen = getAccurateFen();
if (!currentFen) return;
const currentFenHash = hashFen(currentFen);
if (this._premoveProcessedFens.has(currentFenHash)) {
State.statusInfo = "[Premove] Already processed FEN: " + currentFenHash.substring(0, 30);
return;
}
const game = getGame();
if (game && isPlayersTurn(game)) {
this._premoveProcessedFens.add(currentFenHash);
return;
}
if (data.indexOf("info") === 0 && data.includes(" pv ")) {
if (this._premoveProcessing) return;
if (!this._premoveEngineBusy) return;
this._premoveProcessing = true;
State.premoveAnalysisInProgress = true;
try {
const tokens = data.split(" ");
const pvIdx = tokens.indexOf("pv");
const scoreIdx = tokens.indexOf("score");
if (pvIdx === -1) return;
const pvMoves = tokens.slice(pvIdx + 1).filter(m => /^[a-h][1-8][a-h][1-8]/i.test(m));
if (pvMoves.length === 0) return;
let scoreInfo = null;
if (scoreIdx !== -1) {
const type = tokens[scoreIdx + 1];
const value = parseInt(tokens[scoreIdx + 2]);
scoreInfo = {
type,
value,
display: type === 'mate' ? `M${value}` : (value / 100).toFixed(2)
};
State._lastPremoveScoreInfo = scoreInfo;
}
const ourColor = getPlayingAs();
const stm = getCurrentTurn(currentFen);
if (!ourColor || stm === ourColor) return;
const ourUci = getOurMoveFromPV(pvMoves.join(" "), ourColor, stm);
if (!ourUci) return;
if (this._premoveProcessedFens.has(currentFenHash)) return;
const multiPvIdx = tokens.indexOf("multipv");
const multiPv = multiPvIdx !== -1 ? (parseInt(tokens[multiPvIdx + 1], 10) || 1) : 1;
const candidateBucket = this._premoveCandidates[currentFenHash] || [];
const existingCandidateIdx = candidateBucket.findIndex(c => c.multiPv === multiPv);
const candidatePayload = {
multiPv: multiPv,
ourUci: ourUci,
pvMoves: pvMoves.slice(0, 6),
scoreInfo: scoreInfo
};
if (existingCandidateIdx >= 0) candidateBucket[existingCandidateIdx] = candidatePayload;
else candidateBucket.push(candidatePayload);
this._premoveCandidates[currentFenHash] = candidateBucket;
let selectedMove = ourUci;
let selectedDecision = null;
let selectedCandidate = null;
const rankedCandidates = candidateBucket.slice().sort((a, b) => a.multiPv - b.multiPv).slice(0, 2);
for (let ci = 0; ci < rankedCandidates.length; ci++) {
const candidate = rankedCandidates[ci];
const decision = SmartPremove.shouldPremove(currentFen, candidate.ourUci, candidate.pvMoves, candidate.scoreInfo);
if (!selectedDecision || (decision.allowed && (!selectedDecision.allowed || (decision.confidence || 0) > (selectedDecision.confidence || 0)))) {
selectedDecision = decision;
selectedMove = candidate.ourUci;
selectedCandidate = candidate;
}
}
const decision = selectedDecision || SmartPremove.shouldPremove(currentFen, ourUci, pvMoves, scoreInfo);
const finalScoreInfo = selectedCandidate ? selectedCandidate.scoreInfo : scoreInfo;
let confidence = Math.round(decision.confidence || 0);
let requiredConfidence = Math.round((decision.required !== undefined && decision.required !== null) ? decision.required : ((SmartPremove.AGGRESSION_CONFIG[State.premoveMode] || SmartPremove.AGGRESSION_CONFIG.every).minConfidence || 0));
State.premoveLiveChance = clamp(confidence, 0, 100);
State.premoveTargetChance = clamp(requiredConfidence, 0, 100);
State.premoveLastEvalDisplay = finalScoreInfo ? String(finalScoreInfo.display || "-") : "-";
State.premoveLastMoveDisplay = selectedMove ? String(selectedMove).toUpperCase() : "-";
State.premoveChanceReason = decision.allowed ? "Ready" : (decision.reason || "Blocked");
State.premoveChanceUpdatedTs = Date.now();
UI.updatePremoveChanceDisplay();
const firstAttemptForFen = !this._premoveAttemptedFens.has(currentFenHash);
if (firstAttemptForFen) {
this._premoveAttemptedFens.add(currentFenHash);
State.premoveStats.attempted++;
}
if (decision.allowed) {
if (firstAttemptForFen) State.premoveStats.allowed++;
this._premoveProcessedFens.add(currentFenHash);
const MAX_ENGINE_CACHE = Math.min(10, CONFIG.PREMOVE.MAX_EXECUTED_FENS || 50);
if (this._premoveProcessedFens.size > MAX_ENGINE_CACHE) {
const toDelete = this._premoveProcessedFens.size - Math.floor(MAX_ENGINE_CACHE * 0.6);
for (let i = 0; i < toDelete; i++) {
const iter = this._premoveProcessedFens.values();
const first = iter.next().value;
if (first) this._premoveProcessedFens.delete(first);
}
}
let success = await SmartPremove.execute(currentFen, selectedMove, decision);
if (!success) {
this._premoveProcessedFens.delete(currentFenHash);
State.premoveStats.failed++;
} else {
State.premoveStats.executed++;
}
UI.updatePremoveStatsDisplay();
} else {
if (firstAttemptForFen) State.premoveStats.blocked++;
State.statusInfo = `Premove: ${decision.reason}`;
UI.updateStatusInfo();
UI.updatePremoveStatsDisplay();
this._premoveProcessedFens.add(currentFenHash);
}
} catch (e) {
err("[Engine] _onPremoveMessage loop error:", e);
} finally {
this._premoveProcessing = false;
State.premoveAnalysisInProgress = false;
}
}
if (data.indexOf("bestmove") === 0) {
this._premoveEngineBusy = false;
if (this._premoveTimeoutId) {
clearTimeout(this._premoveTimeoutId);
this._premoveTimeoutId = null;
}
const tokens = data.split(" ");
const bestMove = tokens[1];
if (!bestMove || bestMove === "(none)") return;
if (!this._premoveProcessedFens.has(currentFenHash)) {
State.statusInfo = "[Premove] Got bestmove but no execution yet, waiting for PV...";
}
}
},
resetPremoveState: function () {
log("[Engine] Resetting premove state");
this._premoveProcessedFens.clear();
this._premoveAttemptedFens.clear();
this._premoveCandidates = Object.create(null);
this._premoveProcessing = false;
this._premoveEngineBusy = false;
this._premoveLastFen = null;
this._premoveLastActivityTs = Date.now();
if (this._premoveTimeoutId) {
clearTimeout(this._premoveTimeoutId);
this._premoveTimeoutId = null;
}
if (this.premove) {
this.premove.postMessage("stop");
this.premove.postMessage("ucinewgame");
}
},
selfHealPremove: function (reason) {
warn("[Engine] Self-healing premove:", reason || "unknown");
try {
if (this._premoveTimeoutId) {
clearTimeout(this._premoveTimeoutId);
this._premoveTimeoutId = null;
}
if (this.premove) {
try {
this.premove.terminate();
} catch (e) { }
this.premove = null;
}
this._premoveProcessing = false;
this._premoveEngineBusy = false;
this._premoveLastFen = null;
this._premoveLastActivityTs = Date.now();
State.premoveAnalysisInProgress = false;
this.loadPremoveEngine();
} catch (e) {
err("[Engine] selfHealPremove failed:", e);
}
},
selfHealMain: function (reason) {
warn("[Engine] Self-healing main:", reason || "unknown");
try {
this.stop();
this._ready = false;
this._mainLastActivityTs = Date.now();
if (this.main) {
try {
this.main.terminate();
} catch (e) { }
this.main = null;
}
this.loadMainEngine().then(function (ok) {
if (!ok) {
err("[Engine] selfHealMain reload failed");
}
});
} catch (e) {
err("[Engine] selfHealMain failed:", e);
}
},
selfHealAnalysis: function (reason) {
warn("[Engine] Self-healing analysis:", reason || "unknown");
try {
State.isAnalysisThinking = false;
this._analysisLastActivityTs = Date.now();
if (this.analysis) {
try {
this.analysis.terminate();
} catch (e) { }
this.analysis = null;
}
if (State.analysisMode) {
const ok = this.loadAnalysisEngine();
if (!ok) {
err("[Engine] selfHealAnalysis reload failed");
return;
}
State._lastAnalysisFen = null;
}
} catch (e) {
err("[Engine] selfHealAnalysis failed:", e);
}
},
reloadAllEngines: function () {
let self = this;
return new Promise(function (resolve) {
console.log("[Engine] 🔄 Starting full reload sequence...");
self.stop();
if (self.analysis) self.analysis.postMessage("stop");
if (self.premove) self.premove.postMessage("stop");
scheduleManagedTimeout(function () {
self._terminateAllWorkers();
self._revokeAllBlobURLs();
self._resetAllEngineState();
console.log("[Engine] Re-initializing main engine...");
self.init().then(function (mainOk) {
if (!mainOk) {
console.error("[Engine] ❌ Main engine failed");
resolve(false);
return;
}
console.log("[Engine] ✅ Main engine ready");
if (State.analysisMode) {
console.log("[Engine] Loading analysis engine...");
let analysisOk = self.loadAnalysisEngine();
console.log("[Engine]", analysisOk ? "✅ Analysis ready" : "❌ Analysis failed");
}
if (State.premoveEnabled) {
console.log("[Engine] Loading premove engine...");
let premoveOk = self.loadPremoveEngine();
console.log("[Engine]", premoveOk ? "✅ Premove ready" : "❌ Premove failed");
}
let led = $("#engine-status-led");
if (led && self._ready) led.classList.add("active");
console.log("[Engine] 🎉 Full reload complete!");
resolve(true);
});
}, 500);
});
},
_terminateAllWorkers: function () {
console.log("[Engine] Terminating workers...");
[this.main, this.analysis, this.premove].forEach(function (worker, idx) {
if (worker) {
try {
worker.terminate();
console.log("[Engine] Terminated:", ["Main", "Analysis", "Premove"][idx]);
} catch (e) {
console.warn("[Engine] Error terminating:", e);
}
}
});
this.main = null;
this.analysis = null;
this.premove = null;
this._ready = false;
},
_revokeAllBlobURLs: function () {
console.log("[Engine] Revoking blob URLs...");
let self = this;
[this.mainBlobURL, this.analysisBlobURL, this.premoveBlobURL].forEach(function (url, idx) {
if (url) {
self._revokeBlobURL(url);
console.log("[Engine] Revoked:", ["Main", "Analysis", "Premove"][idx]);
}
});
this._revokeAllActiveBlobURLs();
this.mainBlobURL = null;
this.analysisBlobURL = null;
this.premoveBlobURL = null;
},
_resetAllEngineState: function () {
console.log("[Engine] Resetting state...");
this._premoveEngineBusy = false;
this._premoveProcessing = false;
this._premoveProcessedFens.clear();
this._premoveAttemptedFens.clear();
this._premoveCandidates = Object.create(null);
this._premoveLastFen = null;
this._premoveLastActivityTs = Date.now();
if (this._premoveTimeoutId) {
clearTimeout(this._premoveTimeoutId);
this._premoveTimeoutId = null;
}
SmartPremove.resetExecutionTracking();
clearPremoveCaches();
State.isThinking = false;
State.isAnalysisThinking = false;
State.premoveAnalysisInProgress = false;
State.premoveExecutedForFen = null;
State.statusInfo = "Engines reset";
UI.updateStatusInfo();
},
terminate: function () {
let self = this;
let urls = [this.mainBlobURL, this.analysisBlobURL, this.premoveBlobURL];
urls.forEach(function (url) {
if (url) {
self._revokeBlobURL(url);
}
});
this._revokeAllActiveBlobURLs();
this.mainBlobURL = null;
this.analysisBlobURL = null;
this.premoveBlobURL = null;
if (this.main) {
this.main.terminate();
this.main = null;
}
if (this.analysis) {
this.analysis.terminate();
this.analysis = null;
}
if (this.premove) {
this.premove.terminate();
this.premove = null;
}
this._premoveProcessedFens.clear();
this._premoveAttemptedFens.clear();
this._premoveCandidates = Object.create(null);
this._premoveProcessing = false;
this._premoveEngineBusy = false;
}
};
// =====================================================
// Section 30: Engine Module Facade
// =====================================================
Engine.Modules = {
Lifecycle: {
init: Engine.init.bind(Engine),
terminate: Engine.terminate.bind(Engine),
reloadAll: Engine.reloadAllEngines.bind(Engine),
resetPremoveState: Engine.resetPremoveState.bind(Engine)
},
Workers: {
loadMain: Engine.loadMainEngine.bind(Engine),
loadAnalysis: Engine.loadAnalysisEngine.bind(Engine),
loadPremove: Engine.loadPremoveEngine.bind(Engine),
createWorker: Engine._createWorker.bind(Engine)
},
Runtime: {
go: Engine.go.bind(Engine),
stop: Engine.stop.bind(Engine),
onMainMessage: Engine._onMainMessage.bind(Engine),
onAnalysisMessage: Engine._onAnalysisMessage.bind(Engine),
onPremoveMessage: Engine._onPremoveMessage.bind(Engine)
},
Recovery: {
selfHealMain: Engine.selfHealMain.bind(Engine),
selfHealAnalysis: Engine.selfHealAnalysis.bind(Engine),
selfHealPremove: Engine.selfHealPremove.bind(Engine)
},
Config: {
setElo: Engine.setElo.bind(Engine),
setFullStrength: Engine.setFullStrength.bind(Engine),
setSkillLevel: Engine.setSkillLevel.bind(Engine),
configureMain: Engine._configureMainEngine.bind(Engine)
}
};
function isHumanCriticalPosition(fen, ourColor) {
if (!fen || !ourColor) return false;
let oppColor = ourColor === "w" ? "b" : "w";
let ourKing = findKing(fen, ourColor);
if (ourKing) {
let kingAttackers = getAttackersOfSquare(fen, ourKing, oppColor).length;
if (kingAttackers >= (CONFIG.HUMAN.CRITICAL_KING_ATTACKERS || 1)) {
return true;
}
}
let score = State._lastScoreInfo;
if (score && score.type === "mate" && score.value < 0 && Math.abs(score.value) <= (CONFIG.HUMAN.CRITICAL_MATE_PLY || 8)) {
return true;
}
if (score && score.type === "cp" && score.value <= (CONFIG.HUMAN.CRITICAL_CP_THRESHOLD || -120)) {
return true;
}
return false;
}
function getHumanLevelTuning(levelName) {
let tunings = (CONFIG.HUMAN && CONFIG.HUMAN.LEVEL_TUNING) ? CONFIG.HUMAN.LEVEL_TUNING : null;
if (!tunings) {
return { errorMult: 1, blunderMult: 1, criticalErrorMult: 0.55, criticalBlunderMult: 0.20, safetyRiskCap: 60 };
}
return tunings[levelName] || tunings.intermediate ||
{ errorMult: 1, blunderMult: 1, criticalErrorMult: 0.55, criticalBlunderMult: 0.20, safetyRiskCap: 60 };
}
function filterHumanCandidatesBySafety(fen, candidates, ourColor, riskCap) {
if (!fen || !ourColor || !Array.isArray(candidates)) return [];
let cap = typeof riskCap === "number" ? riskCap : 60;
return candidates.filter(function (mv) {
let safety = checkPremoveSafety(fen, mv, ourColor);
return safety && safety.riskLevel < cap;
});
}
function pickHumanFallbackMove(fen, uniqueMoves, level, ourColor) {
if (!Array.isArray(uniqueMoves) || uniqueMoves.length < 2) return null;
let critical = isHumanCriticalPosition(fen, ourColor);
let tuning = getHumanLevelTuning(State.humanLevel || "intermediate");
let errorRate = clamp((level.errorRate || 0) * (tuning.errorMult || 1), 0, 1);
let blunderRate = clamp((level.blunderRate || 0) * (tuning.blunderMult || 1), 0, 1);
let safetyRiskCap = clamp(tuning.safetyRiskCap || 60, 30, 90);
let debug = !!(CONFIG.HUMAN && CONFIG.HUMAN.DEBUG_DECISION);
let selected = null;
if (critical) {
errorRate = clamp(errorRate * (tuning.criticalErrorMult || 0.55), 0, 1);
blunderRate = clamp(blunderRate * (tuning.criticalBlunderMult || 0.20), 0, 1);
}
let softCandidates = uniqueMoves.slice(1, Math.min(3, uniqueMoves.length));
let blunderCandidates = uniqueMoves.slice(3);
if (blunderCandidates.length === 0 && uniqueMoves[2]) {
blunderCandidates = [uniqueMoves[2]];
}
if (critical) {
let safeSoft = filterHumanCandidatesBySafety(fen, softCandidates, ourColor, safetyRiskCap);
if (safeSoft.length > 0) softCandidates = safeSoft;
let safeBlunders = filterHumanCandidatesBySafety(fen, blunderCandidates, ourColor, safetyRiskCap);
if (safeBlunders.length > 0) blunderCandidates = safeBlunders;
} else if (State.humanLevel === "advanced" || State.humanLevel === "expert") {
let saferBlunders = filterHumanCandidatesBySafety(fen, blunderCandidates, ourColor, safetyRiskCap);
if (saferBlunders.length > 0) blunderCandidates = saferBlunders;
}
if (blunderCandidates.length > 0 && Math.random() < blunderRate) {
selected = blunderCandidates[randomInt(0, blunderCandidates.length - 1)];
if (debug) {
log("[HumanMode] blunder pick", selected, "critical=", critical, "level=", State.humanLevel,
"blunderRate=", blunderRate.toFixed(3), "riskCap=", safetyRiskCap);
}
return selected;
}
if (softCandidates.length > 0 && Math.random() < errorRate) {
selected = softCandidates[randomInt(0, softCandidates.length - 1)];
if (debug) {
log("[HumanMode] soft pick", selected, "critical=", critical, "level=", State.humanLevel,
"errorRate=", errorRate.toFixed(3), "riskCap=", safetyRiskCap);
}
return selected;
}
if (debug) {
log("[HumanMode] keep best", uniqueMoves[0], "critical=", critical,
"level=", State.humanLevel,
"errorRate=", errorRate.toFixed(3), "blunderRate=", blunderRate.toFixed(3),
"riskCap=", safetyRiskCap);
}
return null;
}
// =====================================================
// Section 31: Auto Analysis Functions
// =====================================================
function shouldAutoAnalysisMove(bestMoveCandidate) {
if (!State.analysisMode || State.autoAnalysisColor === "none") return false;
if (State._analysisAutoPlayApproved && State._analysisAutoPlayMove === bestMoveCandidate) {
State._analysisAutoPlayApproved = false;
State._analysisAutoPlayMove = null;
State.analysisGuardStateText = "Guard OK (approved)";
UI.updateAnalysisMonitorDisplay();
return true;
}
let fen = getAccurateFen();
if (!fen) return false;
let turn = getCurrentTurn(fen);
let colorMatch = (State.autoAnalysisColor === "white" && turn === "w") ||
(State.autoAnalysisColor === "black" && turn === "b");
if (!colorMatch) return false;
let requiredStable = State.analysisMinStableUpdates || 2;
if (State.analysisStableCount < requiredStable) {
State.analysisGuardStateText = "Stability " + State.analysisStableCount + "/" + requiredStable;
UI.updateAnalysisMonitorDisplay();
return false;
}
if (bestMoveCandidate && State.analysisLastBestMove && bestMoveCandidate !== State.analysisLastBestMove) {
State.analysisGuardStateText = "Bestmove changed";
UI.updateAnalysisMonitorDisplay();
return false;
}
if (State.analysisBlunderGuard &&
typeof State.analysisPrevEvalCp === "number" &&
typeof State.analysisLastEvalCp === "number" &&
State._lastAnalysisDepth >= 8) {
let evalDrop = State.analysisLastEvalCp - State.analysisPrevEvalCp;
if (evalDrop < -300) {
State.analysisGuardStateText = "Blunder guard (" + evalDrop + "cp)";
UI.updateAnalysisMonitorDisplay();
log("[AnalysisGuard] Eval drop " + evalDrop + "cp, blocking move");
return false;
}
}
State._analysisAutoPlayApproved = true;
State._analysisAutoPlayMove = bestMoveCandidate;
State.analysisGuardStateText = "Guard OK";
UI.updateAnalysisMonitorDisplay();
return true;
}
function getMainConsensusMove(fallbackMove) {
if (!State.useMainConsensus) return fallbackMove;
let history = State.mainBestHistory || [];
if (!history.length) return fallbackMove;
let recent = history.slice(-6).filter(function (h) { return h.depth >= 8; });
if (recent.length < 3) return fallbackMove;
let counts = Object.create(null);
recent.forEach(function (h) {
counts[h.move] = (counts[h.move] || 0) + 1;
});
let best = fallbackMove;
let bestCount = 0;
Object.keys(counts).forEach(function (mv) {
if (counts[mv] > bestCount) {
best = mv;
bestCount = counts[mv];
}
});
if (best && bestCount >= 3 && best !== fallbackMove) {
State.statusInfo = "Consensus move selected: " + best + " (" + bestCount + "/" + recent.length + ")";
UI.updateStatusInfo();
return best;
}
return fallbackMove;
}
// =====================================================
// Section 32: Stealth Move Executor
// =====================================================
let MoveExecutor = {
_squareCache: new Map(),
recordMove: function (moveStr) {
if (!moveStr || moveStr.length < 4) return;
let from = moveStr.substring(0, 2);
let to = moveStr.substring(2, 4);
UI.highlightBestMove(from, to);
let currentMoveTime = null;
if (State.moveStartTime && State.moveStartTime > 0) {
currentMoveTime = Date.now() - State.moveStartTime;
State.moveStartTime = 0;
} else if (TimeManager.moveTimes && TimeManager.moveTimes.length > 0) {
currentMoveTime = TimeManager.moveTimes[TimeManager.moveTimes.length - 1];
} else if (State.currentDelayMs && State.currentDelayMs > 0) {
currentMoveTime = State.currentDelayMs;
}
MoveHistory.add(moveStr, State.lastEvalText1, State.customDepth, State.lastMoveGrade, currentMoveTime);
},
movePiece: function (from, to, promotion, isPremove) {
let self = this;
promotion = promotion || "q";
let beforeFen = getAccurateFen();
let isPromo = self._isPromotion(from, to, beforeFen);
if (isPromo) return self._handlePromotionOld(from, to, beforeFen, isPremove);
if (State.moveExecutionMode === "drag" && !isPremove) {
return self._executeHumanizedMove(from, to, beforeFen);
} else {
return self._clickMoveClassic(from, to, beforeFen, isPremove);
}
},
_clickMoveClassic: function (from, to, beforeFen, isPremove) {
let self = this;
let fromCenter = self._getSquareXY(from, true);
let toCenter = self._getSquareXY(to, true);
if (!fromCenter || !toCenter) {
console.error("[ChessAssistant] Cannot get square coordinates:", from, to);
return Promise.resolve(false);
}
let board = getBoardElement();
if (!board) return Promise.resolve(false);
this._squareCache.clear();
return self._dispatchAt(fromCenter, "pointerdown", board)
.then(() => sleep(20))
.then(() => self._dispatchAt(fromCenter, "pointerup", board))
.then(() => sleep(50))
.then(() => self._dispatchAt(toCenter, "pointerdown", board))
.then(() => sleep(20))
.then(() => self._dispatchAt(toCenter, "pointerup", board))
.then(() => true)
.catch(function (e) {
warn("Click strategy failed:", e);
return false;
}).then(function (success) {
if (!success) return false;
if (isPremove) return true;
return self._waitFenChange(beforeFen, 1500);
});
},
_executeHumanizedMove: function (from, to, beforeFen) {
let self = this;
let moveDuration = self._calculateMoveDuration(from, to);
let steps = self._generateBezierPath(from, to, moveDuration);
self._squareCache.clear();
let board = getBoardElement();
let startRect = board ? board.getBoundingClientRect() : null;
let startFlipped = isBoardFlipped();
let rectSig = startRect ? [startRect.left, startRect.top, startRect.width, startRect.height, startFlipped].join("|") : "";
self._dragToken = (self._dragToken || 0) + 1;
const dragToken = self._dragToken;
function boardChanged() {
let b = getBoardElement();
if (!b) return false;
let r = b.getBoundingClientRect();
let sig = [r.left, r.top, r.width, r.height, isBoardFlipped()].join("|");
if (sig !== rectSig) {
rectSig = sig;
return true;
}
return false;
}
return new Promise(function (resolve) {
let stepIndex = 0;
function nextStep() {
if (self._dragToken !== dragToken) {
resolve(false);
return;
}
if (boardChanged()) {
moveDuration = Math.max(220, Math.floor(moveDuration * 0.75));
steps = self._generateBezierPath(from, to, moveDuration);
stepIndex = 0;
}
if (stepIndex >= steps.length) {
scheduleManagedTimeout(function () {
if (self._dragToken !== dragToken) {
resolve(false);
return;
}
self._dispatchAt(steps[steps.length - 1], "pointerup")
.then(() => resolve(true))
.catch(() => resolve(false));
}, randomInt(250, 500));
return;
}
let point = steps[stepIndex];
let eventType = stepIndex === 0 ? "pointerdown" :
stepIndex === steps.length - 1 ? "pointerup" : "pointermove";
self._dispatchAt(point, eventType).then(function () {
stepIndex++;
let delay = Math.floor(moveDuration / steps.length);
delay = Math.max(15, Math.min(40, delay));
scheduleManagedTimeout(nextStep, delay);
}).catch(function () {
resolve(false);
});
}
nextStep();
}).then(function (success) {
if (!success) return false;
return self._waitFenChange(beforeFen, 1500);
});
},
_calculateMoveDuration: function (from, to) {
let fromFile = from.charCodeAt(0) - 97;
let fromRank = parseInt(from[1]);
let toFile = to.charCodeAt(0) - 97;
let toRank = parseInt(to[1]);
let distance = Math.sqrt(Math.pow(toFile - fromFile, 2) + Math.pow(toRank - fromRank, 2));
let baseDuration = 200 + (distance * 100);
let variance = (Math.random() - 0.5) * 80;
if (TimeManager.isTimePressure) baseDuration *= 0.75;
return Math.max(120, Math.min(1200, baseDuration + variance));
},
_generateBezierPath: function (from, to, duration) {
let fromXY = this._getSquareXY(from);
let toXY = this._getSquareXY(to);
if (!fromXY || !toXY) return [fromXY, toXY];
let midX = (fromXY.x + toXY.x) / 2 + (Math.random() - 0.5) * 20;
let midY = (fromXY.y + toXY.y) / 2 + (Math.random() - 0.5) * 20;
let points = [];
let steps = Math.min(40, Math.max(3, Math.floor(duration / 20)));
for (let i = 0; i <= steps; i++) {
let t = i / steps;
t = t * t * (3 - 2 * t);
let x = (1 - t) * (1 - t) * fromXY.x + 2 * (1 - t) * t * midX + t * t * toXY.x;
let y = (1 - t) * (1 - t) * fromXY.y + 2 * (1 - t) * t * midY + t * t * toXY.y;
x += (Math.random() - 0.5) * 0.5;
y += (Math.random() - 0.5) * 0.5;
points.push({ x, y });
}
return points;
},
_clickMove: function (from, to, promotion, isPremove) {
let beforeFen = getAccurateFen();
if (this._isPromotion(from, to, beforeFen)) {
return this._handlePromotionOld(from, to, beforeFen, isPremove);
}
return this._clickMoveClassic(from, to, beforeFen, isPremove);
},
_dispatchAt: function (pos, type, fallbackEl) {
return new Promise(function (resolve) {
if (!pos || typeof pos.x !== 'number' || typeof pos.y !== 'number') {
resolve(); return;
}
let el = document.elementFromPoint(pos.x, pos.y) || fallbackEl;
if (!el) { resolve(); return; }
let board = getBoardElement();
let isBoardChild = board && (el === board || board.contains(el));
let isBoardParent = board && el.contains && el.contains(board);
if (!isBoardChild && !isBoardParent) {
resolve(); return;
}
if (el.closest && (el.closest('#chess-assist-panel') || el.closest('.cap-overlay'))) {
resolve(); return;
}
let isDown = type.includes("down");
let isUp = type.includes("up");
let isMove = type.includes("move");
let options = {
bubbles: true,
cancelable: true,
composed: true,
clientX: pos.x,
clientY: pos.y,
button: 0,
buttons: (isDown || isMove) ? 1 : 0,
pointerId: 1,
pointerType: "mouse",
isPrimary: true,
pressure: isDown ? 0.5 : (isMove ? 0.3 : 0),
detail: 1
};
try {
if (typeof PointerEvent === 'function') {
if (isDown) {
el.dispatchEvent(new PointerEvent("pointerover", options));
el.dispatchEvent(new PointerEvent("pointerenter", options));
}
el.dispatchEvent(new PointerEvent(type, options));
}
if (isDown) {
el.dispatchEvent(new MouseEvent("mousedown", options));
} else if (isUp) {
el.dispatchEvent(new MouseEvent("mouseup", options));
el.dispatchEvent(new MouseEvent("click", options));
} else if (isMove) {
el.dispatchEvent(new MouseEvent("mousemove", options));
}
} catch (e) {
}
resolve();
});
},
_getSquareXY: function (square, addOffset) {
let board = getBoardElement();
if (!board) return null;
let useCache = addOffset !== false;
let cacheKey = square + (isBoardFlipped() ? "_f" : "_n");
if (useCache && this._squareCache.has(cacheKey)) return this._squareCache.get(cacheKey);
let rect = board.getBoundingClientRect();
let file = square.charCodeAt(0) - 97;
let rank = parseInt(square.charAt(1)) - 1;
let flipped = isBoardFlipped();
let squareSize = rect.width / 8;
let xIdx, yIdx;
if (flipped) {
xIdx = 7 - file;
yIdx = rank;
} else {
xIdx = file;
yIdx = 7 - rank;
}
let offsetX = 0, offsetY = 0;
if (addOffset !== false) {
offsetX = (Math.random() - 0.5) * squareSize * 0.6;
offsetY = (Math.random() - 0.5) * squareSize * 0.6;
}
let result = {
x: rect.left + (xIdx + 0.5) * squareSize + offsetX,
y: rect.top + (yIdx + 0.5) * squareSize + offsetY
};
if (useCache) this._squareCache.set(cacheKey, result);
return result;
},
_waitFenChange: function (prevFen, timeout) {
let start = Date.now();
let check = function () {
if (Date.now() - start >= timeout) return Promise.resolve(false);
let current = getAccurateFen();
if (current !== prevFen) return Promise.resolve(true);
return sleep(50).then(check);
};
return check();
},
_handlePromotionOld: async function (from, to, beforeFen, isPremove) {
let self = this;
let moved = await self._clickMoveClassic(from, to, beforeFen, true);
if (!moved) return false;
let promoDetected = false;
for (let i = 0; i < 30; i++) {
let found =
document.querySelector(".promotion-piece") ||
document.querySelector("[data-cy='promotion-queen']") ||
document.querySelector(".promotion-window") ||
document.querySelector("[class*='promotion']");
if (found) {
promoDetected = true;
break;
}
await sleep(50);
}
if (promoDetected) {
let promoted = await self._handlePromotionDialog();
if (!promoted) return false;
}
if (isPremove) return true;
return self._waitFenChange(beforeFen, 2500);
},
_isPromotion: function (from, to, fen) {
if (!fen) return false;
let piece = this._getPieceAt(from, fen);
if (!piece) return false;
let rankTo = parseInt(to.charAt(1));
return (piece === "P" && rankTo === 8) || (piece === "p" && rankTo === 1);
},
_getPieceAt: function (square, fen) {
if (!fen || !square) return null;
let rows = fen.split(" ")[0].split("/");
let file = square.charCodeAt(0) - 97;
let rank = 8 - parseInt(square.charAt(1));
if (rank < 0 || rank > 7 || file < 0 || file > 7) return null;
let col = 0;
for (let i = 0; i < rows[rank].length; i++) {
let ch = rows[rank][i];
if (/\d/.test(ch)) {
col += parseInt(ch);
} else {
if (col === file) return ch;
col++;
}
}
return null;
},
_handlePromotionDialog: async function () {
for (let attempt = 0; attempt < 40; attempt++) {
let queenBtn =
document.querySelector('[data-cy="promotion-queen"]') ||
document.querySelector('.promotion-piece.wq') ||
document.querySelector('.promotion-piece.bq') ||
document.querySelector('[data-piece="q"]');
if (queenBtn) {
let rect = queenBtn.getBoundingClientRect();
let x = rect.left + rect.width / 2;
let y = rect.top + rect.height / 2;
let opts = {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
button: 0
};
queenBtn.dispatchEvent(new PointerEvent("pointerdown", opts));
queenBtn.dispatchEvent(new MouseEvent("mousedown", opts));
queenBtn.dispatchEvent(new PointerEvent("pointerup", opts));
queenBtn.dispatchEvent(new MouseEvent("mouseup", opts));
queenBtn.dispatchEvent(new MouseEvent("click", opts));
return true;
}
await sleep(50);
}
return false;
},
};
// =====================================================
// Section 33: Auto Move Execution
// =====================================================
function executeAction(selectedUci, analysisFen) {
if (!selectedUci || selectedUci.length < 4) return;
let from = selectedUci.substring(0, 2);
let to = selectedUci.substring(2, 4);
let promotionChar = selectedUci.length >= 5 ? selectedUci[4] : null;
if (!State.autoMovePiece) return;
let game = getGame();
if (!game || !isPlayersTurn(game)) {
State.statusInfo = "Waiting for opponent";
UI.updateStatusInfo();
return;
}
cancelPendingMove();
let Delay = getCalculatedDelay();
State.moveStartTime = Date.now();
State.statusInfo = "Moving in " + (Delay / 1000).toFixed(1) + "s";
UI.updateStatusInfo();
pendingMoveTimeoutId = scheduleManagedTimeout(function () {
pendingMoveTimeoutId = null;
if (!_allLoopsActive) return;
let freshGame = getGameController();
if (!freshGame || !isPlayersTurn(freshGame)) {
State.moveExecutionInProgress = false;
State.statusInfo = "Move canceled (not our turn)";
UI.updateStatusInfo();
return;
}
let currentFen = getAccurateFen();
if (currentFen !== analysisFen) {
State.moveExecutionInProgress = false;
State.statusInfo = "Move canceled (position changed)";
UI.updateStatusInfo();
return;
}
State.moveExecutionInProgress = true;
State.statusInfo = "Making move...";
UI.updateStatusInfo();
MoveExecutor.movePiece(from, to, promotionChar, false).then(function (success) {
State.moveExecutionInProgress = false;
State.statusInfo = success ? "Move made!" : "Move failed";
UI.updateStatusInfo();
if (!success) {
scheduleManagedTimeout(function () {
if (State.autoRun && isPlayersTurn(getGame())) runEngineNow();
}, 800);
}
}).catch(function () {
State.moveExecutionInProgress = false;
});
}, Delay);
}
// =====================================================
// Section 34: ACPL Tracking
// =====================================================
let ACPL = {
onNewEval: function (newCp, mateVal) {
if (!State.acplInitialized) {
State.previousEvaluation = newCp;
State.acplInitialized = true;
return;
}
let fen = getAccurateFen();
if (!fen) return;
let turnToMove = getCurrentTurn(fen);
let whoJustMoved = turnToMove === "w" ? "b" : "w";
let cpl = 0;
if (whoJustMoved === "w") {
cpl = Math.max(0, State.previousEvaluation - newCp);
} else {
cpl = Math.max(0, newCp - State.previousEvaluation);
}
State.lastMoveGrade = this._grade(cpl, mateVal !== null);
let prevIsMate = Math.abs(State.previousEvaluation) >= CONFIG.MATE_VALUE;
let curIsMate = Math.abs(newCp) >= CONFIG.MATE_VALUE;
if (!prevIsMate && !curIsMate) {
if (whoJustMoved === "w") {
State.totalCplWhite += cpl;
State.cplMoveCountWhite++;
State.acplWhite = (State.totalCplWhite / State.cplMoveCountWhite / 100).toFixed(2);
} else {
State.totalCplBlack += cpl;
State.cplMoveCountBlack++;
State.acplBlack = (State.totalCplBlack / State.cplMoveCountBlack / 100).toFixed(2);
}
}
State.previousEvaluation = newCp;
UI.updateACPL();
},
_grade: function (cpl, isMateRelated) {
if (isMateRelated || cpl <= 5) return "Terbaik";
if (cpl < 25) return "Sangat Baik";
if (cpl < 75) return "Bagus";
if (cpl < 150) return "Kurang Tepat";
if (cpl < 250) return "Kesalahan";
return "Blunder";
},
reset: function () {
State.totalCplWhite = 0;
State.cplMoveCountWhite = 0;
State.acplWhite = "0.00";
State.totalCplBlack = 0;
State.cplMoveCountBlack = 0;
State.acplBlack = "0.00";
State.previousEvaluation = 0;
State.acplInitialized = false;
State.lastMoveGrade = "Book";
clearPremoveCaches();
UI.updateACPL();
}
};
// =====================================================
// Section 35: Opening Book Management
// =====================================================
function weightedRandomMove(movesObj) {
if (!movesObj) return null;
let total = Object.values(movesObj).reduce((a, b) => a + (typeof b === 'number' ? b : (b.weight || 0)), 0);
if (total === 0) return Object.keys(movesObj)[0] || null;
let rand = Math.random() * total;
for (let move in movesObj) {
let weight = typeof movesObj[move] === 'number' ? movesObj[move] : (movesObj[move].weight || 0);
rand -= weight;
if (rand < 0) return move;
}
return Object.keys(movesObj)[0] || null;
}
let OpeningBook = {
_noEpIndex: null,
_noEpIndexVersion: -1,
_firstMoveNames: {
e2e4: "King's Pawn Opening",
d2d4: "Queen's Pawn Game",
c2c4: "English Opening",
g1f3: "Réti Opening",
f2f4: "Bird's Opening",
b2b3: "Nimzowitsch-Larsen Attack",
b2b4: "Polish Opening",
g2g3: "King's Indian Attack"
},
_buildNoEpIndex: function () {
if (this._noEpIndex && this._noEpIndexVersion === _openingBookVersion) return;
this._noEpIndex = new Map();
let keys = Object.keys(OPENING_BOOK);
for (let i = 0; i < keys.length; i++) {
let parts = keys[i].split(" ");
let key3 = parts.slice(0, 3).join(" ");
if (!this._noEpIndex.has(key3)) {
this._noEpIndex.set(key3, OPENING_BOOK[keys[i]]);
}
}
this._noEpIndexVersion = _openingBookVersion;
},
_getOpeningName: function (fen, move, history) {
if (!move) return "Book Move";
if (OPENING_NAMES[move]) {
return OPENING_NAMES[move];
}
let startFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -";
if (normalizeFen(fen) === startFen && this._firstMoveNames[move]) {
return this._firstMoveNames[move];
}
if (Array.isArray(history) && history.length > 0) {
let firstMove = history[0];
if (firstMove && this._firstMoveNames[firstMove]) {
return this._firstMoveNames[firstMove];
}
}
return "Book Move";
},
getMove: function (fen, history) {
if (!State.useOpeningBook || !fen) return null;
let notationMove = this.getNotationMove(history);
if (notationMove) {
let display = $("#currentOpeningDisplay");
if (display) {
display.textContent = "Notation Book";
display.style.color = "#FFD700";
}
State.statusInfo = "Notation move: " + notationMove;
UI.updateStatusInfo();
return notationMove;
}
let key = normalizeFen(fen);
let movesObj = OPENING_BOOK[key];
if (!movesObj) {
this._buildNoEpIndex();
let parts = fen.split(" ");
let key3 = parts.slice(0, 3).join(" ");
movesObj = this._noEpIndex.get(key3);
}
if (!movesObj) return null;
let move = weightedRandomMove(movesObj);
if (!move) return null;
let name = this._getOpeningName(fen, move, history);
let display = $("#currentOpeningDisplay");
if (display) {
display.textContent = name;
display.style.color = "#1E90FF";
}
State.statusInfo = "Opening book move: " + move + " (" + name + ")";
UI.updateStatusInfo();
return move;
},
getNotationMove: function (history) {
if (!State.notationSequence || !history || history.length === 0) return null;
let sequence = State.notationSequence.replace(/\d+\./g, " ").trim().split(/\s+/);
if (sequence.length === 0) return null;
for (let i = 0; i < history.length; i++) {
if (i >= sequence.length || history[i] !== sequence[i]) {
return null;
}
}
if (history.length < sequence.length) {
return sequence[history.length];
}
return null;
}
};
// =====================================================
// Section 36: Move History and Records
// =====================================================
let MoveHistory = {
add: function (move, evalText, depth, grade, moveTime, source) {
let tbody = $("#moveHistoryTableBody");
if (!tbody) return;
let moveNum = tbody.children.length + 1;
let row = document.createElement("tr");
let evalClass = "eval-equal";
if (typeof evalText === "string") {
if (evalText.includes("M")) evalClass = "eval-mate";
else {
let v = parseFloat(evalText);
if (!isNaN(v)) evalClass = v > 0.4 ? "eval-positive" : v < -0.4 ? "eval-negative" : "eval-equal";
}
}
let gradeColors = {
"Terbaik": "#7fa650", "Bagus": "#4caf50", "Cukup Baik": "#aeea00",
"Tidak Akurat": "#ffc107", "Kesalahan": "#ff9800", "Blunder": "#f44336", "Book": "#888"
};
let gc = gradeColors[grade] || "#888";
let safeMove = escapeHtml(String(move || ""));
let safeEval = escapeHtml(String(evalText || "0.00"));
let safeGrade = escapeHtml(String(grade || "Book"));
let safeSource = escapeHtml(String(source || "Engine"));
let timerDisplay = "-";
if (typeof moveTime === "number" && moveTime > 0) {
timerDisplay = (moveTime / 1000).toFixed(2) + "s";
}
let safeTime = escapeHtml(String(timerDisplay || "-"));
row.innerHTML =
"" + moveNum + " " +
"" + safeMove + " " +
"" + safeEval + " " +
"" + (depth || "-") + " " +
"" + safeGrade + " " +
"" + safeSource + " " +
"" + safeTime + " ";
tbody.insertBefore(row, tbody.firstChild);
while (tbody.children.length > CONFIG.MAX_HISTORY_SIZE) tbody.removeChild(tbody.lastChild);
let filterInput = $("#inp-move-filter");
if (filterInput && UI && typeof UI.applyMoveHistoryFilter === "function") {
UI.applyMoveHistoryFilter(filterInput.value || "");
}
},
clear: function () {
let tbody = $("#moveHistoryTableBody");
if (tbody) tbody.innerHTML = "";
ACPL.reset();
}
};
// =====================================================
// Section 37: User Interface Management
// =====================================================
let UI = {
_arrowElements: [],
_pvArrowElements: [],
_bestmoveArrowElements: [],
_arrowLimit: 60,
_lastDrawnIsAnalysis: null,
_lastHighlightedMove: null,
_lastArrowMoves: null,
_isStreamProof: false,
_panicMode: false,
_panicHotkeysBound: false,
_lastHeartbeatTs: 0,
touchHeartbeat: function () { this._lastHeartbeatTs = Date.now(); },
initPanicKey: function () {
if (this._panicHotkeysBound) return;
this._panicHotkeysBound = true;
let self = this;
let panicHotkeysHandler = function (e) {
if (e.ctrlKey && e.shiftKey && e.key === 'H') { e.preventDefault(); self.togglePanicMode(); }
if (e.ctrlKey && e.shiftKey && e.key === 'S') { e.preventDefault(); self.toggleStreamProof(); }
if (e.ctrlKey && e.shiftKey && e.key === 'M') { e.preventDefault(); self.toggleMoveMode(); }
if (e.ctrlKey && e.shiftKey && (e.key === '?' || e.key === '/')) { e.preventDefault(); self.showHotkeyHelp(); }
};
document.addEventListener('keydown', panicHotkeysHandler);
_eventListeners.push({ element: document, type: 'keydown', handler: panicHotkeysHandler });
},
togglePanicMode: function () {
this._panicMode = !this._panicMode;
let panel = $("#chess-assist-panel");
if (this._panicMode) {
if (panel) panel.style.opacity = '0';
this.clearAll(); this._removeAllVisuals();
State.statusInfo = "PANIC MODE - All hidden";
} else {
if (panel) panel.style.opacity = '1';
State.statusInfo = "Normal mode restored";
}
UI.updateStatusInfo();
},
toggleStreamProof: function () {
this._isStreamProof = !this._isStreamProof;
if (this._isStreamProof) { this._applyStreamProofStyles(); State.statusInfo = "Stream-proof mode ON"; }
else { this._removeStreamProofStyles(); State.statusInfo = "Stream-proof mode OFF"; }
UI.updateStatusInfo();
},
toggleMoveMode: function () {
let newMode = State.moveExecutionMode === "click" ? "drag" : "click";
State.moveExecutionMode = newMode;
saveSetting("moveExecutionMode", newMode);
State.statusInfo = "Move Mode: " + newMode.toUpperCase() + (newMode === "drag" ? " (Bezier)" : " (Simple)");
UI.updateStatusInfo();
},
showHotkeyHelp: function () {
let existing = $("#cap-hotkeys-overlay");
if (existing) return;
let overlay = document.createElement("div");
overlay.id = "cap-hotkeys-overlay";
overlay.className = "cap-hotkeys-overlay";
overlay.innerHTML =
'' +
'
Hotkeys
' +
'
Shortcut Action ' +
'Ctrl+Shift+H Toggle Panic Mode ' +
'Ctrl+Shift+S Toggle Stream Proof ' +
'Ctrl+Shift+M Toggle Move Execution Mode ' +
'Ctrl+Shift+? Open Hotkeys Help ' +
'Alt + A..Z Set Engine Depth (1..26) ' +
'Esc Toggle panel open/closed ' +
'
' +
'
Close
' +
'
';
document.body.appendChild(overlay);
let closeBtn = $("#btn-hotkeys-close", overlay);
if (closeBtn) { closeBtn.addEventListener("click", function () { if (overlay.parentNode) overlay.parentNode.removeChild(overlay); }); }
overlay.addEventListener("click", function (e) { if (e.target === overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); } });
},
applyMoveHistoryFilter: function (query) {
let tbody = $("#moveHistoryTableBody");
if (!tbody) return;
let q = String(query || "").trim().toLowerCase();
let rows = Array.from(tbody.querySelectorAll("tr"));
rows.forEach(function (row) { let text = (row.textContent || "").toLowerCase(); row.style.display = !q || text.includes(q) ? "" : "none"; });
},
_hardRemoveElements: function (list, selector) {
if (Array.isArray(list)) { list.forEach(function (el) { try { if (el && el.classList) el.classList.remove("fade"); if (el && typeof el.remove === "function") el.remove(); else if (el && el.parentNode) el.parentNode.removeChild(el); } catch (e) { } }); list.length = 0; }
if (selector) { try { $$(selector).forEach(function (el) { try { if (el && typeof el.remove === "function") el.remove(); else if (el && el.parentNode) el.parentNode.removeChild(el); } catch (e) { } }); } catch (e) { } }
},
_limitArrowArray: function (list, max) {
if (!Array.isArray(list)) return;
if (list.length <= max) return;
let overflow = list.length - max;
for (let i = 0; i < overflow; i++) { let el = list.shift(); try { if (el && typeof el.remove === "function") el.remove(); else if (el && el.parentNode) el.parentNode.removeChild(el); } catch (e) { } }
},
_applyStreamProofStyles: function () {
let style = document.getElementById('stream-proof-styles');
if (!style) {
style = document.createElement('style');
style.id = 'stream-proof-styles';
style.textContent = `.chess-assist-arrow rect{stroke-width:2px!important;opacity:0.4!important;filter:none!important}.chess-assist-pv-arrow line{stroke-width:2px!important;opacity:0.3!important}.chess-assist-pv-arrow circle{r:6!important;opacity:0.4!important}.chess-assist-pv-arrow text{display:none!important}`;
document.head.appendChild(style);
}
},
_removeStreamProofStyles: function () { let style = document.getElementById('stream-proof-styles'); if (style) style.remove(); },
_removeAllVisuals: function () {
let arrows = document.querySelectorAll(".chess-assist-arrow, .chess-assist-pv-arrow, .chess-assist-bestmove-arrow");
arrows.forEach(function (el) { el.remove(); });
this._arrowElements = []; this._pvArrowElements = []; this._bestmoveArrowElements = [];
},
updateMove: function (num, move, evalText, evalClass) {
let moveEl = $("#topMove" + num); let evalEl = $("#topMoveEval" + num);
if (moveEl) moveEl.textContent = move || "...";
if (evalEl) { evalEl.textContent = evalText || "0.00"; evalEl.className = "eval " + (evalClass || "eval-equal"); }
},
_refreshEvalBarStatus: function () {
let status = this._resolveEvalBarStatus();
if (State._lastShownEvalBarStatus === status) return;
State._lastShownEvalBarStatus = status;
if (typeof State._lastEvalBarCp === "number") {
this.updateEvalBar(State._lastEvalBarCp, State._lastEvalBarMate, State._lastEvalBarDepth);
}
},
_resolveEvalBarStatus: function () {
if (State.moveExecutionInProgress) {
let elapsed = State.moveStartTime ? ((Date.now() - State.moveStartTime) / 1000).toFixed(1) : "0.0";
return "Moving in " + elapsed + "s";
}
if (!Engine || (!Engine._ready && !Engine.main)) return "Engine Loading...";
if (!Engine._ready && Engine.main) return "Engine Init...";
if (State.isThinking) return "Analyzing...";
if (State.isAnalysisThinking) return "Analysis Running";
if (!isPlayersTurn()) return "Opponent's Turn";
if (State.autoRun) return "⏳ Waiting";
return "Ready";
},
updateEvalBar: function (rawCp, mateVal, depth) {
let fill = $("#evaluationFillAutoRun"); let text = $("#autoRunStatusText");
if (!fill || !text) return;
State._lastEvalBarCp = rawCp; State._lastEvalBarMate = mateVal; State._lastEvalBarDepth = depth;
fill.style.transition = "width 0.5s ease, background-color 0.5s ease";
let pct = 50, color = "#9E9E9E", label = "0.00", emo = "";
let deltaCp = 0;
if (typeof rawCp === "number") {
if (typeof State._lastEvalRawCp === "number") { deltaCp = rawCp - State._lastEvalRawCp; }
State._lastEvalRawCp = rawCp; State.lastEvalDeltaCp = deltaCp;
}
if (mateVal !== null) {
pct = mateVal > 0 ? 100 : 0; color = mateVal > 0 ? "#4CAF50" : "#FF4500";
label = (mateVal > 0 ? "M+" : "M") + mateVal; emo = mateVal > 0 ? "😊 Unggul" : "😟 Tertekan";
State.evalBarInitialized = false;
} else {
if (!State.evalBarInitialized) { State.evalBarSmoothedCp = rawCp; State.evalBarInitialized = true; }
else { State.evalBarSmoothedCp = (State.evalBarSmoothedCp * 0.75) + (rawCp * 0.25); }
let smoothCp = State.evalBarSmoothedCp;
let capped = clamp(smoothCp, -CONFIG.MAX_BAR_CAP, CONFIG.MAX_BAR_CAP);
pct = 50 + (capped / CONFIG.MAX_BAR_CAP) * 50;
color = smoothCp >= 0 ? "#4CAF50" : "#FF4500";
label = (smoothCp >= 0 ? "+" : "") + (smoothCp / 100).toFixed(2);
if (Math.abs(smoothCp) < 20) { emo = "😐 Seimbang"; color = "#9E9E9E"; }
else { emo = smoothCp > 0 ? "😊 Unggul" : "😟 Tertekan"; }
}
fill.style.width = pct + "%"; fill.style.backgroundColor = color;
let status = this._resolveEvalBarStatus();
let deltaText = "";
if (typeof State.lastEvalDeltaCp === "number" && State.lastEvalDeltaCp !== 0) {
let deltaPawn = (State.lastEvalDeltaCp / 100).toFixed(2);
if (State.lastEvalDeltaCp > 0) deltaText = " Δ+" + deltaPawn; else deltaText = " Δ" + deltaPawn;
}
let safeLabel = escapeHtml(String(label)); let safeEmo = escapeHtml(String(emo)); let safeStatus = escapeHtml(String(status));
text.innerHTML = "" + safeLabel + " " + safeEmo + " " +
"D" + (depth || 0) + deltaText + " " + safeStatus + " ";
},
updateAnalysisBar: function (rawCp) {
let fill = $("#evaluationFillAnalysis"); if (!fill) return;
let analysisFen = State._lastAnalysisFen || getAccurateFen();
let stm = getCurrentTurn(analysisFen);
let whiteCp = stm === "b" ? -rawCp : rawCp;
let pct = 50;
if (Math.abs(whiteCp) >= CONFIG.MATE_VALUE) { pct = whiteCp > 0 ? 100 : 0; }
else { let capped = clamp(whiteCp, -CONFIG.MAX_BAR_CAP, CONFIG.MAX_BAR_CAP); pct = 50 + (capped / CONFIG.MAX_BAR_CAP) * 50; }
fill.style.width = pct + "%"; fill.style.backgroundColor = "#f8fafc";
},
updateACPL: function () {
let el = $("#acplTextDisplay"); if (el) el.textContent = "W " + State.acplWhite + " / B " + State.acplBlack;
let wc = $("#cplMoveCountWhiteDisplay"); let bc = $("#cplMoveCountBlackDisplay");
if (wc) wc.textContent = State.cplMoveCountWhite; if (bc) bc.textContent = State.cplMoveCountBlack;
let wBar = $("#acplBarWhite"); let bBar = $("#acplBarBlack");
if (wBar && bBar) {
let wCp = Math.min(parseFloat(State.acplWhite) * 100, CONFIG.MAX_ACPL_DISPLAY);
let bCp = Math.min(parseFloat(State.acplBlack) * 100, CONFIG.MAX_ACPL_DISPLAY);
wBar.style.width = (wCp / CONFIG.MAX_ACPL_DISPLAY * 100) + "%";
bBar.style.width = (bCp / CONFIG.MAX_ACPL_DISPLAY * 100) + "%";
}
},
updatePVDisplay: function () { let el = $("#pvDisplay"); if (!el) return; el.textContent = State.principalVariation && State.principalVariation.length > 0 ? State.principalVariation : "Waiting for analysis..."; },
updateStatusInfo: function () {
this.touchHeartbeat();
let statusEl = $('#infoStatus'); if (!statusEl) return;
let statusText = State.statusInfo || 'Ready';
statusEl.textContent = statusText;
statusEl.classList.remove('ready', 'analyzing', 'waiting', 'error', 'countdown');
let statusLower = statusText.toLowerCase();
if (statusLower.includes('⏳') || statusLower.includes('moving in')) { statusEl.classList.add('countdown'); statusEl.style.color = '#f9e2af'; statusEl.style.fontWeight = 'bold'; }
else if (statusLower.includes('ready') || statusLower.includes('✓')) { statusEl.classList.add('ready'); statusEl.style.color = '#a6e3a1'; }
else if (statusLower.includes('analyz') || statusLower.includes('🔄')) { statusEl.classList.add('analyzing'); statusEl.style.color = '#89b4fa'; }
else if (statusLower.includes('wait') || statusLower.includes('⏳')) { statusEl.classList.add('waiting'); statusEl.style.color = '#fab387'; }
else if (statusLower.includes('error') || statusLower.includes('❌')) { statusEl.classList.add('error'); statusEl.style.color = '#f38ba8'; }
else { statusEl.style.color = '#cdd6f4'; }
},
updatePremoveChanceDisplay: function (game, rawCp, evalText, bestMove, moveNumber) {
let chanceEl = document.getElementById('premoveChanceDisplay'); if (!chanceEl) return;
if (!State.premoveEnabled) { chanceEl.innerHTML = '- '; return; }
if (typeof rawCp === 'number' && typeof evalText !== 'undefined' && typeof bestMove === 'string') {
let ourColor = getPlayingAs(game || getGame());
let baselineChance = Math.round(Number(getEvalBasedPremoveChance(rawCp / 100, ourColor)) || 0);
let displayNum = typeof moveNumber === 'number' ? moveNumber : 1;
if (State.premoveChanceUpdatedTs <= 0) { State.premoveLiveChance = baselineChance; }
State.premoveLastEvalDisplay = String(evalText || '0.00');
State.premoveLastMoveDisplay = bestMove.length >= 4 ? bestMove.substring(0, 4).toUpperCase() : String(bestMove).toUpperCase();
State.premoveChanceReason = 'Tracking';
if (!State.premoveTargetChance || State.premoveTargetChance <= 0) {
let modeCfg = SmartPremove.AGGRESSION_CONFIG[State.premoveMode] || SmartPremove.AGGRESSION_CONFIG.every;
State.premoveTargetChance = clamp(Math.round(modeCfg.minConfidence || 0), 0, 100);
}
State.premoveLastMoveDisplay = "#" + displayNum + " " + State.premoveLastMoveDisplay;
}
if (State.premoveChanceUpdatedTs <= 0) {
let ourColorFallback = getPlayingAs(game || getGame());
let fallbackChance = Math.round(Number(getEvalBasedPremoveChance((Number(State.currentEvaluation) || 0) / 100, ourColorFallback)) || 0);
State.premoveLiveChance = clamp(fallbackChance, 0, 100);
let modeCfgFallback = SmartPremove.AGGRESSION_CONFIG[State.premoveMode] || SmartPremove.AGGRESSION_CONFIG.every;
State.premoveTargetChance = clamp(Math.round(modeCfgFallback.minConfidence || 0), 0, 100);
State.premoveChanceReason = "Waiting for engine PV";
if (!State.premoveLastEvalDisplay || State.premoveLastEvalDisplay === "-") { State.premoveLastEvalDisplay = typeof State.currentEvaluation === 'number' ? (State.currentEvaluation / 100).toFixed(2) : "-"; }
}
let liveChance = clamp(Math.round(Number(State.premoveLiveChance) || 0), 0, 100);
let targetChance = clamp(Math.round(Number(State.premoveTargetChance) || 0), 0, 100);
let progressToTarget = targetChance > 0 ? clamp(Math.round((liveChance / targetChance) * 100), 0, 100) : 100;
let chanceColor = liveChance >= targetChance ? '#a6e3a1' : liveChance <= 20 ? '#ff9800' : '#f9e2af';
let safeEval = escapeHtml(String(State.premoveLastEvalDisplay || '-'));
let safeMove = escapeHtml(String(State.premoveLastMoveDisplay || '-'));
let safeReason = escapeHtml(String(State.premoveChanceReason || 'Tracking'));
chanceEl.innerHTML =
'#Premove ' +
'[Eval: ' + safeEval + ' ] ' +
'[Move: ' + safeMove + ' ] ' +
'[Chance: ' + liveChance + '% ] ' +
'[Target: ' + targetChance + '% ] ' +
'[Progress: ' + progressToTarget + '% ] ' +
'(' + safeReason + ') ';
chanceEl.style.color = '#cdd6f4';
},
updatePremoveStatsDisplay: function () {
let el = $("#premoveStatsDisplay"); if (!el) return;
let s = State.premoveStats || { attempted: 0, allowed: 0, executed: 0, blocked: 0, failed: 0 };
el.textContent = "A:" + s.attempted + " OK:" + s.allowed + " EX:" + s.executed + " BL:" + s.blocked + " FL:" + s.failed;
},
updateCCTDebugDisplay: function () {
let el = $("#cctDebugDisplay"); if (!el) return;
if (!State.cctDebugEnabled) { el.textContent = "CCT debug disabled"; el.style.opacity = "0.72"; return; }
let txt = State.cctLastDebugText || "CCT debug idle"; el.textContent = String(txt); el.style.opacity = "1";
},
updateAnalysisMonitorDisplay: function () {
let stableEl = $("#analysis-stability-indicator"); let guardEl = $("#analysis-guard-indicator");
if (stableEl) { stableEl.textContent = (State.analysisStableCount || 0) + "x"; }
if (guardEl) {
guardEl.textContent = State.analysisGuardStateText || "Ready";
let txt = (State.analysisGuardStateText || "").toLowerCase();
if (txt.includes("blocked")) guardEl.style.color = "#f38ba8";
else if (txt.includes("waiting") || txt.includes("changed")) guardEl.style.color = "#f9e2af";
else guardEl.style.color = "#a6e3a1";
}
},
updateDiagnosticsDisplay: function () {
this.touchHeartbeat();
let workersEl = $("#diag-workers"); let cachesEl = $("#diag-caches"); let runtimeEl = $("#diag-runtime"); let errorsEl = $("#diag-errors"); let selfTestEl = $("#diag-selftest");
if (!workersEl && !cachesEl && !runtimeEl && !errorsEl && !selfTestEl) return;
let report = getDiagnosticsSnapshot();
if (workersEl) { workersEl.textContent = "M:" + (report.workers.main ? "ON" : "OFF") + " A:" + (report.workers.analysis ? "ON" : "OFF") + " P:" + (report.workers.premove ? "ON" : "OFF"); }
if (cachesEl) { cachesEl.textContent = "PR:" + report.caches.premoveProcessedFens + " CCT:" + report.caches.cctCache + " TH:" + report.caches.threatCache; }
if (runtimeEl) { runtimeEl.textContent = "H(P/M/A):" + report.runtime.premoveHealCount + "/" + report.runtime.mainHealCount + "/" + report.runtime.analysisHealCount + " L:" + (State.loopStarted ? 1 : 0); }
if (errorsEl) { let cnt = ErrorTelemetry.moduleCounts; errorsEl.textContent = "E En:" + (cnt.engine || 0) + " UI:" + (cnt.ui || 0) + " Pr:" + (cnt.premove || 0) + " Sy:" + (cnt.syzygy || 0) + " Rt:" + (cnt.runtime || 0) + " O:" + (cnt.other || 0); }
if (selfTestEl) { selfTestEl.textContent = "SelfTest R:" + (report.runtime.selfTestRuns || 0) + " F:" + (report.runtime.selfTestFailures || 0) + " UIH:" + (report.runtime.uiHealCount || 0) + " LH:" + (report.runtime.listenerHealCount || 0); }
},
updateSyzygyDisplay: function () {
let statusEl = $("#syzygyStatus"); let bodyEl = $("#syzygyTableBody"); if (!statusEl || !bodyEl) return;
let meta = State.syzygyMeta || null; let source = State.syzygySource ? (" [" + State.syzygySource + "]") : ""; let metaText = "";
if (meta) { let cat = meta.category ? String(meta.category).toUpperCase() : "UNKNOWN"; let dtz = (typeof meta.dtz === "number") ? (" DTZ " + meta.dtz) : ""; let dtm = (typeof meta.dtm === "number") ? (" DTM " + meta.dtm) : ""; metaText = " | " + cat + dtz + dtm; }
let errText = State.syzygyError ? (" | " + String(State.syzygyError)) : "";
statusEl.textContent = String(State.syzygyStatus || "Idle") + source + metaText + errText;
let moves = Array.isArray(State.syzygyMoves) ? State.syzygyMoves : [];
if (!moves.length) { bodyEl.innerHTML = 'No tablebase line '; return; }
let rows = "";
for (let i = 0; i < moves.length; i++) { let mv = moves[i] || {}; let uci = escapeHtml(String(mv.uci || mv.san || "-")); let category = escapeHtml(String(mv.category || "-")); let dtz = (typeof mv.dtz === "number") ? ("DTZ " + mv.dtz) : ""; let dtm = (typeof mv.dtm === "number") ? ("DTM " + mv.dtm) : ""; let score = escapeHtml((dtz + (dtz && dtm ? " | " : "") + dtm) || "-"); rows += "" + (i + 1) + " " + uci + " " + category + " " + score + " "; }
bodyEl.innerHTML = rows;
},
highlightBestMove: function (from, to, isAnalysis) {
this._removeHighlightSquares(); if (!State.highlightEnabled) return;
if (!from || !to || from.length !== 2 || to.length !== 2) { warn("Invalid highlight move:", from, to); return; }
let color = isAnalysis ? State.highlightColor2 : State.highlightColor1;
this._drawSquareHighlight(from, to, color, isAnalysis);
this._lastHighlightedMove = { from: from, to: to, isAnalysis: isAnalysis, time: Date.now() };
},
highlightMove: function (move, color, isAnalysis) {
this._removeHighlightSquares(); if (!move || move.length < 4) { warn("Invalid move for highlight:", move); return; }
let from = move.substring(0, 2); let to = move.substring(2, 4);
let actualColor = isAnalysis ? State.highlightColor2 : color;
this._drawSquareHighlight(from, to, actualColor, isAnalysis);
this._lastHighlightedMove = { from: from, to: to, move: move, isAnalysis: isAnalysis, time: Date.now() };
},
_removeHighlightSquares: function () { this._hardRemoveElements(this._arrowElements, ".chess-assist-arrow"); },
_drawSquareHighlight: function (from, to, color, isAnalysis) {
let board = getBoardElement(); if (!board) return;
let fromXY = MoveExecutor._getSquareXY(from, false); let toXY = MoveExecutor._getSquareXY(to, false);
if (!fromXY || !toXY) return;
let container = (board.tagName && board.tagName.toLowerCase() === "wc-chess-board") ? (board.parentElement || board) : board;
let boardRect = board.getBoundingClientRect(); let containerRect = container.getBoundingClientRect(); let squareSize = boardRect.width / 8;
let fx = fromXY.x - containerRect.left - squareSize / 2; let fy = fromXY.y - containerRect.top - squareSize / 2;
let tx = toXY.x - containerRect.left - squareSize / 2; let ty = toXY.y - containerRect.top - squareSize / 2;
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("class", "chess-assist-arrow");
let zIndex = isAnalysis ? 10001 : 9999;
svg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:" + zIndex + ";";
svg.setAttribute("data-analysis", isAnalysis ? "true" : "false");
let glowSize = isAnalysis ? 6 : 4; let opacity = isAnalysis ? "0.98" : "0.95";
let borderRadius = Math.max(4, squareSize * 0.15); let fontSize = Math.max(10, squareSize * 0.15);
svg.innerHTML =
" " +
" " +
"" + from.toUpperCase() + " " +
"" + to.toUpperCase() + " ";
container.style.position = container.style.position || "relative";
container.appendChild(svg);
this._arrowElements.push(svg);
this._limitArrowArray(this._arrowElements, this._arrowLimit);
},
drawPVArrows: function (pvMoves, startingTurn, isAnalysis) {
if (!pvMoves || pvMoves.length === 0 || !State.showPVArrows) return;
let validMoves = pvMoves.filter(function (m) { return m && m.length >= 4 && /^[a-h][1-8][a-h][1-8]/.test(m); });
if (validMoves.length === 0) return;
let board = getBoardElement(); if (!board) return;
let container = (board.tagName && board.tagName.toLowerCase() === "wc-chess-board") ? (board.parentElement || board) : board;
let pvStr = validMoves.join(" "); let now = Date.now();
let lastRendered = isAnalysis ? State.lastRenderedAnalysisPV : State.lastRenderedMainPV;
let lastDrawTime = isAnalysis ? State.lastAnalysisPVDrawTime : State.lastMainPVDrawTime;
if (pvStr === lastRendered && now - lastDrawTime < 100) return;
if (this._lastDrawnIsAnalysis !== isAnalysis) { this._removePVArrowsByType(!isAnalysis); }
this._removePVArrowsByType(isAnalysis);
if (isAnalysis) { State.lastRenderedAnalysisPV = pvStr; State.lastAnalysisPVDrawTime = now; }
else { State.lastRenderedMainPV = pvStr; State.lastMainPVDrawTime = now; }
this._lastDrawnIsAnalysis = isAnalysis;
this._doPVDraw(validMoves, startingTurn, pvStr, board, container, isAnalysis);
},
drawBestmoveArrows: function () {
if (!State.showBestmoveArrows) return;
let board = getBoardElement(); if (!board) return;
let container = (board.tagName && board.tagName.toLowerCase() === "wc-chess-board") ? (board.parentElement || board) : board;
this.clearBestmoveArrows();
let infos = State.topMoveInfos || {}; let moveCount = clamp(State.numberOfMovesToShow || 5, 2, 10);
let frag = document.createDocumentFragment();
for (let i = 1; i <= moveCount; i++) {
let info = infos[i]; if (!info || !info.move || info.move.length < 4) continue;
let from = info.move.substring(0, 2); let to = info.move.substring(2, 4);
let badge = info.evalText || ""; let alpha = i === 1 ? 0.95 : Math.max(0.55, 0.9 - (i * 0.08));
let bmColors = State.bestmoveArrowColors || {};
let basePalette = [bmColors[1] || bmColors["1"] || State.bestmoveArrowColor || "#eb6150", bmColors[2] || bmColors["2"] || "#89b4fa", bmColors[3] || bmColors["3"] || "#a6e3a1", bmColors[4] || bmColors["4"] || "#f38ba8", bmColors[5] || bmColors["5"] || "#cba6f7", bmColors[6] || bmColors["6"] || "#fab387", bmColors[7] || bmColors["7"] || "#74c7ec", bmColors[8] || bmColors["8"] || "#f5c2e7", bmColors[9] || bmColors["9"] || "#b4befe"];
let color = basePalette[(i - 1) % 9] || "#f9e2af";
let arrow = this._createBestmoveArrowSVG(from, to, color, alpha, i, badge, board, container);
if (arrow) { this._bestmoveArrowElements.push(arrow); frag.appendChild(arrow); this._limitArrowArray(this._bestmoveArrowElements, this._arrowLimit); }
}
if (this._bestmoveArrowElements.length > 0) { container.style.position = container.style.position || "relative"; container.appendChild(frag); }
},
_createBestmoveArrowSVG: function (from, to, color, opacity, rank, badge, board, container) {
let fromXY = MoveExecutor._getSquareXY(from, false); let toXY = MoveExecutor._getSquareXY(to, false);
if (!fromXY || !toXY) return null;
let containerRect = (container || board).getBoundingClientRect();
let x1 = fromXY.x - containerRect.left, y1 = fromXY.y - containerRect.top, x2 = toXY.x - containerRect.left, y2 = toXY.y - containerRect.top;
let uid = "bm-" + rank + "-" + Date.now(); let angle = Math.atan2(y2 - y1, x2 - x1);
let strokeWidth = 4, circleRadius = 10, arrowHeadSize = Math.max(8, strokeWidth * 2);
let markerWidth = Math.max(6, strokeWidth * 1.5), markerHeight = Math.max(4, strokeWidth);
let endX = x2 - Math.cos(angle) * arrowHeadSize, endY = y2 - Math.sin(angle) * arrowHeadSize;
let midX = (x1 + endX) / 2, midY = (y1 + endY) / 2;
let safeBadge = escapeHtml(String(badge || ""));
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("class", "chess-assist-bestmove-arrow");
svg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:" + (9980 + rank) + ";";
let badgeWidth = Math.max(22, safeBadge.length * 4.5 + 6);
let badgeSvg = safeBadge ? "" + safeBadge + " " : "";
svg.innerHTML = " " +
" " +
" " +
" " +
" " +
"" + rank + " " + badgeSvg;
return svg;
},
_doPVDraw: function (pvMoves, startingTurn, pvStr, board, container, isAnalysis) {
this._lastDrawnIsAnalysis = isAnalysis;
let maxMoves = Math.min(pvMoves.length, State.maxPVDepth); let frag = document.createDocumentFragment();
let pvColors = State.pvArrowColors || {};
let pvPalette = [pvColors[1] || pvColors["1"] || "#4287f5", pvColors[2] || pvColors["2"] || "#eb6150", pvColors[3] || pvColors["3"] || "#4caf50", pvColors[4] || pvColors["4"] || "#9c27b0", pvColors[5] || pvColors["5"] || "#f38ba8", pvColors[6] || pvColors["6"] || "#fab387", pvColors[7] || pvColors["7"] || "#74c7ec", pvColors[8] || pvColors["8"] || "#f5c2e7", pvColors[9] || pvColors["9"] || "#b4befe"];
this._lastArrowMoves = [];
for (let i = 0; i < maxMoves; i++) {
let move = pvMoves[i]; if (!move || move.length < 4) continue;
let from = move.substring(0, 2), to = move.substring(2, 4);
let opacity = Math.max(0.55, 0.95 - (i * 0.05)); let color = pvPalette[i % pvPalette.length];
this._lastArrowMoves.push({ from: from, to: to, index: i });
let evalBadge = "";
if (i === 0) {
let cpValue = null;
if (State.topMoveInfos && State.topMoveInfos[1] && typeof State.topMoveInfos[1].rawCp === "number") { cpValue = State.topMoveInfos[1].rawCp; }
else if (typeof State.currentEvaluation === "number") { cpValue = State.currentEvaluation; }
if (typeof cpValue === "number" && isFinite(cpValue) && Math.abs(cpValue) < CONFIG.MATE_VALUE) { let pct = 50 + (45 * Math.tanh(cpValue / 300)); evalBadge = pct.toFixed(1) + "%"; }
else if (State.lastEvalText1) { evalBadge = State.lastEvalText1; }
}
let el = this._createPVArrowSVG(from, to, color, opacity, 4, i, board, container, isAnalysis, evalBadge);
if (el) { this._pvArrowElements.push(el); frag.appendChild(el); this._limitArrowArray(this._pvArrowElements, this._arrowLimit); }
}
container.style.position = container.style.position || "relative"; container.appendChild(frag);
},
_createPVArrowSVG: function (from, to, color, opacity, strokeWidth, index, board, container, isAnalysis, evalBadge) {
function getContrastColor(hexColor) { if (!hexColor) return "#000000"; let c = hexColor.replace("#", ""); if (c.length === 3) { c = c.split("").map(function (x) { return x + x; }).join(""); } let r = parseInt(c.substr(0, 2), 16), g = parseInt(c.substr(2, 2), 16), b = parseInt(c.substr(4, 2), 16); let brightness = (r * 299 + g * 587 + b * 114) / 1000; return brightness > 150 ? "#000000" : "#ffffff"; }
let fromXY = MoveExecutor._getSquareXY(from, false); let toXY = MoveExecutor._getSquareXY(to, false);
if (!fromXY || !toXY) return null;
let containerRect = (container || board).getBoundingClientRect();
let x1 = fromXY.x - containerRect.left, y1 = fromXY.y - containerRect.top, x2 = toXY.x - containerRect.left, y2 = toXY.y - containerRect.top;
let angle = Math.atan2(y2 - y1, x2 - x1); let arrowHeadSize = Math.max(8, strokeWidth * 2);
let endX = x2 - Math.cos(angle) * arrowHeadSize, endY = y2 - Math.sin(angle) * arrowHeadSize;
let uid = "pv-" + (isAnalysis ? "a-" : "m-") + index + "-" + Date.now();
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("class", "chess-assist-pv-arrow"); svg.setAttribute("data-analysis", isAnalysis ? "true" : "false");
let zIndex = 9990 + index + (isAnalysis ? 100 : 0);
svg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:" + zIndex + ";";
let blurAmount = isAnalysis ? "2px" : "1.5px", circleRadius = isAnalysis ? 12 : 10, textYOffset = isAnalysis ? 5 : 4;
let markerWidth = Math.max(6, strokeWidth * 1.5), markerHeight = Math.max(4, strokeWidth);
let textColor = getContrastColor(color), textStroke = textColor === "#ffffff" ? "#000000" : "#ffffff";
let numberBgOpacity = 0.85, midX = (x1 + endX) / 2, midY = (y1 + endY) / 2;
let safeEvalBadge = escapeHtml(String(evalBadge || "")); let evalBadgeWidth = Math.max(24, safeEvalBadge.length * 4.4 + 6);
let evalBadgeSvg = safeEvalBadge ? "" + safeEvalBadge + " " : "";
svg.innerHTML =
" " +
" " +
" " +
" " +
" " +
" " +
"" + (index + 1) + " " + evalBadgeSvg;
return svg;
},
_removePVArrowsByType: function (isAnalysis) {
for (let i = this._pvArrowElements.length - 1; i >= 0; i--) { let el = this._pvArrowElements[i]; let elIsAnalysis = el.getAttribute("data-analysis") === "true"; if (elIsAnalysis === isAnalysis) { if (el.parentNode) el.parentNode.removeChild(el); this._pvArrowElements.splice(i, 1); } }
},
_removePVArrowsDOM: function () {
this._pvArrowElements.forEach(function (el) { if (el.parentNode) el.parentNode.removeChild(el); }); this._pvArrowElements = [];
$$(".chess-assist-pv-arrow").forEach(function (el) { el.remove(); });
},
clearBestmoveArrows: function () {
this._bestmoveArrowElements.forEach(function (el) { if (el.parentNode) el.parentNode.removeChild(el); }); this._bestmoveArrowElements = [];
$$(".chess-assist-bestmove-arrow").forEach(function (el) { el.remove(); });
},
clearPVArrows: function () {
this._pvArrowElements = []; this._removePVArrowsDOM();
State.lastRenderedMainPV = ""; State.lastRenderedAnalysisPV = ""; State.lastMainPVDrawTime = 0; State.lastAnalysisPVDrawTime = 0;
this._lastDrawnIsAnalysis = null; this._lastArrowMoves = null;
},
clearHighlights: function () { this._removeHighlightSquares(); this._lastHighlightedMove = null; },
clearAll: function () {
this._removeHighlightSquares(); this._removePVArrowsDOM(); this.clearBestmoveArrows();
this._lastDrawnIsAnalysis = null; this._lastHighlightedMove = null; this._lastArrowMoves = null;
State.lastRenderedMainPV = ""; State.lastRenderedAnalysisPV = ""; State.lastMainPVDrawTime = 0; State.lastAnalysisPVDrawTime = 0;
},
updateTurnLEDs: function () {
let myTurnLed = $("#GILIRAN-SAYA"); let oppTurnLed = $("#GILIRAN-LAWAN"); let engineLed = $("#engine-status-led");
if (engineLed) engineLed.classList.toggle("active", State.isThinking || State.isAnalysisThinking);
if (!myTurnLed || !oppTurnLed) return;
let myTurn = isPlayersTurn(); myTurnLed.classList.toggle("active", myTurn); oppTurnLed.classList.toggle("active", !myTurn);
},
updateClock: function () {
this.touchHeartbeat(); const clock = $("#digital-clock");
if (clock) { const timeString = new Date().toLocaleTimeString("en-US", { hour12: false }); clock.textContent = timeString; }
}
};
// =====================================================
// Section 38: UI Module Facade
// =====================================================
UI.Modules = {
Core: { updateStatusInfo: UI.updateStatusInfo.bind(UI), updateClock: UI.updateClock.bind(UI), updateTurnLEDs: UI.updateTurnLEDs.bind(UI), clearAll: UI.clearAll.bind(UI) },
Visuals: { drawPVArrows: UI.drawPVArrows.bind(UI), drawBestmoveArrows: UI.drawBestmoveArrows.bind(UI), clearPVArrows: UI.clearPVArrows.bind(UI), clearBestmoveArrows: UI.clearBestmoveArrows.bind(UI), clearHighlights: UI.clearHighlights.bind(UI), removeAllVisuals: UI._removeAllVisuals.bind(UI) },
Analysis: { updateAnalysisBar: UI.updateAnalysisBar.bind(UI), updateAnalysisMonitorDisplay: UI.updateAnalysisMonitorDisplay.bind(UI), updatePVDisplay: UI.updatePVDisplay.bind(UI) },
History: { updateACPL: UI.updateACPL.bind(UI), applyMoveHistoryFilter: UI.applyMoveHistoryFilter.bind(UI), updateMove: UI.updateMove.bind(UI) },
Diagnostics: { updateDiagnosticsDisplay: UI.updateDiagnosticsDisplay.bind(UI), updateCCTDebugDisplay: UI.updateCCTDebugDisplay.bind(UI), touchHeartbeat: UI.touchHeartbeat.bind(UI) },
Modes: { togglePanicMode: UI.togglePanicMode.bind(UI), toggleStreamProof: UI.toggleStreamProof.bind(UI), toggleMoveMode: UI.toggleMoveMode.bind(UI), showHotkeyHelp: UI.showHotkeyHelp.bind(UI) }
};
// =====================================================
// Section 39: Auto Resignation Logic
// =====================================================
let _resignInProgress = false;
function resetResignState() {
_resignTriggerCount = 0;
_resignInProgress = false;
if (_resignTimeout) { clearTimeout(_resignTimeout); _resignTimeout = null; }
if (_resignObserver) { _resignObserver.disconnect(); _resignObserver = null; }
}
function checkAutoResign(scoreType, scoreValue) {
if (!State.autoResignEnabled || State.gameEnded || _resignInProgress) return;
let trigger = false;
if (State.resignMode === "cp") {
if (scoreType === "cp" && scoreValue <= -Math.abs(State.autoResignThresholdCp)) trigger = true;
} else if (State.resignMode === "mate") {
if (scoreType === "mate" && scoreValue < 0) {
if (Math.abs(scoreValue) <= State.autoResignThresholdMate) trigger = true;
}
}
if (trigger) {
_resignTriggerCount++;
State.statusInfo = "Auto-resign: " + scoreType + " " + scoreValue + " (" + _resignTriggerCount + "/" + _resignTriggerNeeded + ")";
UI.updateStatusInfo();
if (_resignTriggerCount >= _resignTriggerNeeded && !_resignTimeout) {
_resignInProgress = true;
log("[AutoResign] Threshold reached, resigning in 1.5s...");
_resignTimeout = setTimeout(resignGame, 1500);
}
} else {
if (_resignTriggerCount > 0) _resignTriggerCount--;
}
}
function _clickButtonRobust(button) {
if (!button) return false;
try {
let rect = button.getBoundingClientRect();
let cx = rect.left + rect.width / 2;
let cy = rect.top + rect.height / 2;
let opts = { bubbles: true, cancelable: true, clientX: cx, clientY: cy, button: 0, buttons: 1 };
button.dispatchEvent(new PointerEvent("pointerdown", Object.assign({ pointerId: 1, pointerType: "mouse", isPrimary: true }, opts)));
button.dispatchEvent(new MouseEvent("mousedown", opts));
button.dispatchEvent(new PointerEvent("pointerup", Object.assign({ pointerId: 1, pointerType: "mouse", isPrimary: true }, opts)));
button.dispatchEvent(new MouseEvent("mouseup", opts));
button.dispatchEvent(new MouseEvent("click", opts));
return true;
} catch (e) { }
try { button.click(); return true; } catch (e2) { }
return false;
}
function _findResignButton() {
let selectors = [
'button[data-cy="resign-button-with-confirmation"]',
'button[data-cy="resign-button"]',
'button[aria-label="Resign"]',
'button[data-cy="game-controls-resign"]',
'.resign-button'
];
for (let i = 0; i < selectors.length; i++) {
let el = document.querySelector(selectors[i]);
if (el) return el;
}
return Array.from(document.querySelectorAll("button")).find(function (btn) {
let text = (btn.getAttribute("aria-label") || btn.textContent || "").trim().toLowerCase();
return text === "resign" || text === "menyerah";
}) || null;
}
function _findResignConfirmButton() {
let selectors = [
'[data-cy="popover-resign-confirmation"] button[data-cy="confirmation-popover-confirm-button"]',
'[data-cy="confirmation-popover"] button[data-cy="confirmation-popover-confirm-button"]',
'button[data-cy="confirmation-popover-confirm-button"]',
'button[data-cy="confirm-yes"]',
'button[data-cy="confirm-modal-primary-button"].cc-button-danger'
];
for (let i = 0; i < selectors.length; i++) {
let el = document.querySelector(selectors[i]);
if (el) return el;
}
return Array.from(document.querySelectorAll("button")).find(function (el) {
if (!el || el.disabled) return false;
let dataCy = (el.getAttribute("data-cy") || "").toLowerCase();
let cls = (el.className || "").toLowerCase();
let text = (el.textContent || "").trim().toLowerCase();
return dataCy.includes("confirmation-popover-confirm-button") ||
(cls.includes("cc-button-danger") && (text === "menyerah" || text === "resign"));
}) || null;
}
function resignGame() {
if (State.gameEnded) { resetResignState(); return; }
let cleanupDone = false;
function cleanup() {
if (cleanupDone) return;
cleanupDone = true;
_resignInProgress = false;
if (_resignObserver) { _resignObserver.disconnect(); _resignObserver = null; }
if (_resignTimeout) { clearTimeout(_resignTimeout); _resignTimeout = null; }
}
let resignButton = _findResignButton();
if (!resignButton) {
warn("[AutoResign] Resign button not found");
cleanup();
return;
}
_resignObserver = new MutationObserver(function () {
let confirmBtn = _findResignConfirmButton();
if (confirmBtn) {
log("[AutoResign] Confirm button found via observer");
_clickButtonRobust(confirmBtn);
State.gameEnded = true;
cleanup();
}
});
let modalContainer = document.querySelector('.modal-container') || document.body;
_resignObserver.observe(modalContainer, { childList: true, subtree: true });
log("[AutoResign] Clicking resign button");
_clickButtonRobust(resignButton);
let immediateConfirm = _findResignConfirmButton();
if (immediateConfirm) {
log("[AutoResign] Confirm found immediately");
_clickButtonRobust(immediateConfirm);
State.gameEnded = true;
cleanup();
return;
}
let pollCount = 0;
let pollMax = 10;
function pollConfirm() {
if (cleanupDone || State.gameEnded) return;
pollCount++;
let btn = _findResignConfirmButton();
if (btn) {
log("[AutoResign] Confirm found via polling (attempt " + pollCount + ")");
_clickButtonRobust(btn);
State.gameEnded = true;
cleanup();
return;
}
if (pollCount < pollMax) {
scheduleManagedTimeout(pollConfirm, 500);
} else {
warn("[AutoResign] Confirm button not found after " + pollMax + " polls");
cleanup();
}
}
scheduleManagedTimeout(pollConfirm, 500);
_resignTimeout = setTimeout(function () {
if (!cleanupDone) {
warn("[AutoResign] Timeout — cleanup");
cleanup();
}
}, 8000);
}
// =====================================================
// Section 40: Auto Match System
// =====================================================
let AutoMatch = {
inProgress: false, lastAttemptTime: 0, attemptCount: 0, MAX_ATTEMPTS: 5, ACTION_DELAY_MS: 5000, INITIAL_WAIT_MS: 3000, POLL_INTERVAL_MS: 1000, POLL_TIMEOUT_MS: 20000, CLICK_SETTLE_MS: 800, LANGUAGE_MODE: "auto",
_isAllowedContext: function () { let pathname = String(window.location.pathname || ""); let allowedPathHints = ["/play", "/game", "/live", "/daily", "/computer"]; let hasAllowedPath = allowedPathHints.some(function (p) { return pathname.indexOf(p) !== -1; }); let hasBoard = !!getBoardElement(); let hasKnownGameShell = !!(document.querySelector("#board-layout-chessboard") || document.querySelector(".board-layout-chessboard") || document.querySelector("[data-cy='board-layout']")); if (hasAllowedPath && (hasBoard || hasKnownGameShell)) return true; if (String(window.location.href || "").indexOf("chess.com/play") !== -1 && hasBoard) return true; return false; },
_resolveLexicon: function () { let mode = (this.LANGUAGE_MODE || "auto").toLowerCase(); let docLang = String(document.documentElement && document.documentElement.lang || "").toLowerCase(); let bodyText = String((document.body && document.body.textContent) || "").toLowerCase(); if (mode !== "auto") { return mode === "id" ? this._LEXICON_ID : this._LEXICON_EN; } let idSignals = ["menyerah", "batal", "tanding ulang", "game baru", "pertandingan baru", "tolak"]; let idScore = idSignals.reduce(function (acc, w) { return acc + (bodyText.indexOf(w) !== -1 ? 1 : 0); }, 0); if (docLang.indexOf("id") !== -1 || idScore >= 2) { return this._LEXICON_ID; } return this._LEXICON_EN; },
_LEXICON_EN: { decline: ["decline", "reject", "no"], newGame: ["new game", "new 10 min", "new 5 min", "new 3 min", "new 1 min", "new 15 min", "new 30 min"], rematch: ["rematch", "play again"] },
_LEXICON_ID: { decline: ["tolak", "reject", "tidak", "batal"], newGame: ["baru", "game baru", "pertandingan baru", "main baru", "mnt baru", "10 mnt baru", "5 mnt baru", "3 mnt baru", "1 mnt baru", "15 mnt baru", "30 mnt baru"], rematch: ["tanding ulang", "main lagi", "rematch"] },
_visible: function (el) { if (!el) return false; return el.offsetParent !== null && el.offsetWidth > 0 && el.offsetHeight > 0 && !el.disabled && getComputedStyle(el).visibility !== 'hidden' && getComputedStyle(el).display !== 'none'; },
_isButtonClickable: function (el) { if (!el) return false; let rect = el.getBoundingClientRect(); return rect.width > 0 && rect.height > 0 && rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth; },
_findButton: function (selectors, predicate, priorityText) {
let candidates = []; let self = this;
for (let si = 0; si < selectors.length; si++) { let nodes; try { nodes = $$(selectors[si]); } catch (e) { continue; } for (let ni = 0; ni < nodes.length; ni++) { let el = nodes[ni]; if (!self._visible(el)) continue; let txt = (el.textContent || el.innerText || "").trim().toLowerCase(); let ariaLabel = (el.getAttribute("aria-label") || "").toLowerCase(); let title = (el.getAttribute("title") || "").toLowerCase(); let dataCy = (el.getAttribute("data-cy") || "").toLowerCase(); let score = 0; if (predicate) { if (predicate(txt, el)) score += 10; if (predicate(ariaLabel, el)) score += 8; if (predicate(title, el)) score += 6; } if (priorityText && priorityText.length > 0) { for (let pt of priorityText) { let ptLower = pt.toLowerCase(); if (txt === ptLower) score += 25; else if (txt.includes(ptLower)) score += 15; if (ariaLabel === ptLower) score += 20; else if (ariaLabel.includes(ptLower)) score += 12; } } if (dataCy.includes("decline")) score += 30; if (dataCy.includes("new-game")) score += 25; if (dataCy.includes("rematch")) score += 20; if (score > 0) { candidates.push({ el, score, text: txt }); } } }
candidates.sort((a, b) => b.score - a.score);
if (candidates.length > 0) { log("[AutoMatch] Best candidate:", candidates[0].text, "score:", candidates[0].score); return candidates[0].el; } return null;
},
_clickElement: function (el, description) {
if (!el) { warn("[AutoMatch] No element to click:", description); return false; }
log("[AutoMatch] Clicking:", description, "| Text:", el.textContent?.trim());
try { let rect = el.getBoundingClientRect(); let centerX = rect.left + rect.width / 2; let centerY = rect.top + rect.height / 2; const events = [['pointerdown', { bubbles: true, cancelable: true, clientX: centerX, clientY: centerY, pointerId: 1, pointerType: 'mouse', isPrimary: true }], ['mousedown', { bubbles: true, cancelable: true, clientX: centerX, clientY: centerY, button: 0, buttons: 1 }], ['pointerup', { bubbles: true, cancelable: true, clientX: centerX, clientY: centerY, pointerId: 1, pointerType: 'mouse' }], ['mouseup', { bubbles: true, cancelable: true, clientX: centerX, clientY: centerY, button: 0 }], ['click', { bubbles: true, cancelable: true, clientX: centerX, clientY: centerY }]]; for (let [eventType, options] of events) { let event; if (eventType.startsWith('pointer')) { event = new PointerEvent(eventType, options); } else { event = new MouseEvent(eventType, options); } el.dispatchEvent(event); } scheduleManagedTimeout(() => { try { el.click(); } catch (e) { } }, 50); return true; } catch (e) { err("[AutoMatch] Click error:", e); try { el.click(); return true; } catch (e2) { return false; } }
},
_detectGameOver: function () {
const selectors = [".game-result-component", ".game-over-modal-content", ".game-over-modal-component", ".game-over-ad-component", ".game-over-ad-container-component", ".daily-game-footer-game-over", "[data-cy='game-over-modal']", "[data-cy='game-result-modal']", ".game-over-secondary-actions-row-component", ".game-over-buttons-component", ".game-over-modal-buttons", "[data-cy='game-over-modal-new-game-button']", "[data-cy='game-over-modal-rematch-button']", "[data-cy='rematch-button']", "[data-cy='rematch-request-modal']", "[data-cy='rematch-offer-modal']", ".rematch-request-component", ".rematch-offer-component"];
let self = this; for (let sel of selectors) { let el = $(sel); if (el && self._visible(el)) { log("[AutoMatch] Game over detected via:", sel); return true; } }
let newGameBtn = $("[data-cy='game-over-modal-new-game-button']"); let rematchBtn = $("[data-cy='game-over-modal-rematch-button']"); let newBotBtn = $("[data-cy='game-over-modal-new-bot-button']"); let declineBtn = $("[data-cy='rematch-decline-button']");
if ((newGameBtn && self._visible(newGameBtn)) || (rematchBtn && self._visible(rematchBtn)) || (newBotBtn && self._visible(newBotBtn)) || (declineBtn && self._visible(declineBtn))) { log("[AutoMatch] Game over detected via buttons"); return true; } return false;
},
_detectRematchRequest: function () {
let self = this;
const declineSelectors = ["[data-cy='rematch-decline-button']", "[data-cy='decline-rematch-button']", "button[data-cy*='decline']", "button[data-cy*='reject']"];
const requestModalSelectors = ["[data-cy='rematch-request-modal']", "[data-cy='rematch-offer-modal']", ".rematch-request-component", ".rematch-offer-component", ".rematch-dialog-component"];
for (let sel of requestModalSelectors) { let el = $(sel); if (el && self._visible(el)) { return true; } }
for (let sel of declineSelectors) { let el = $(sel); if (el && self._visible(el)) { return true; } } return false;
},
_findActionableButton: function () {
let lex = this._resolveLexicon();
let self = this;
let directBtn = document.querySelector("[data-cy='game-over-modal-new-game-button']");
if (directBtn && self._visible(directBtn) && self._isButtonClickable(directBtn)) {
let txt = (directBtn.textContent || "").trim().toLowerCase();
let isRematch = (lex.rematch || []).some(function (kw) { return txt.includes(kw); });
if (!isRematch) {
log("[AutoMatch] Direct match: data-cy new-game-button, text: " + txt);
return { el: directBtn, type: "newgame" };
}
}
if (this._detectRematchRequest()) {
let declineBtn = this._findButton(["[data-cy='rematch-decline-button']", "[data-cy='decline-rematch-button']", "button[data-cy*='decline']", "button[data-cy*='reject']", "button[data-cy*='tolak']"], function (txt) { return (lex.decline || []).some(function (kw) { return txt.includes(kw); }); }, (lex.decline || []).map(function (w) { return w.charAt(0).toUpperCase() + w.slice(1); }));
if (declineBtn && this._isButtonClickable(declineBtn)) return { el: declineBtn, type: "decline" };
}
let newGameBtn = this._findButton(["[data-cy='game-over-modal-new-game-button']", "[data-cy='new-game-button']", "button[data-cy*='new-game']", "button[data-cy*='new_game']"], function (txt) { let hasNewGame = (lex.newGame || []).some(function (kw) { return txt.includes(kw); }); let isRematch = (lex.rematch || []).some(function (kw) { return txt.includes(kw); }) || txt.includes("lagi") || txt.includes("tanding"); return hasNewGame && !isRematch; }, (lex.newGame || []).map(function (w) { return w.split(" ").map(function (p) { return p.charAt(0).toUpperCase() + p.slice(1); }).join(" "); }));
if (newGameBtn && this._isButtonClickable(newGameBtn)) return { el: newGameBtn, type: "newgame" };
let fallbackBtn = this._findButton(["button", "a[role='button']", "[role='button']"], function (txt, el) {
let dataCy = (el.getAttribute("data-cy") || "").toLowerCase();
if (dataCy.includes("rematch")) return false;
let ariaLabel = (el.getAttribute("aria-label") || "").toLowerCase();
if (ariaLabel.includes("tanding") || ariaLabel.includes("rematch")) return false;
if (txt.includes("tanding") || txt.includes("rematch") || txt.includes("lagi")) return false;
return (lex.newGame || []).some(function (kw) { return txt.includes(kw); });
});
if (fallbackBtn && this._isButtonClickable(fallbackBtn)) return { el: fallbackBtn, type: "fallback" };
return null;
},
try: function () {
let now = Date.now();
if (!this._isAllowedContext()) { this.attemptCount = 0; return; }
if (now - this.lastAttemptTime < this.ACTION_DELAY_MS) return;
this.lastAttemptTime = now;
if (this.inProgress) return;
if (!this._detectGameOver()) { this.attemptCount = 0; return; }
this.inProgress = true;
this.attemptCount++;
log("[AutoMatch] Attempt", this.attemptCount, "— waiting for button...");
let self = this;
let pollStart = Date.now();
sleep(self.INITIAL_WAIT_MS).then(function () {
return new Promise(function (resolve) {
function poll() {
if (!_allLoopsActive || !State.autoMatch) { resolve(false); return; }
if (Date.now() - pollStart > self.POLL_TIMEOUT_MS) {
warn("[AutoMatch] Poll timeout — no button found in " + self.POLL_TIMEOUT_MS + "ms");
resolve(false);
return;
}
let found = self._findActionableButton();
if (found) {
log("[AutoMatch] Button found: " + found.type + " — settling " + self.CLICK_SETTLE_MS + "ms");
sleep(self.CLICK_SETTLE_MS).then(function () {
let recheck = self._findActionableButton();
if (recheck && recheck.el === found.el && self._isButtonClickable(recheck.el)) {
resolve(recheck);
} else {
log("[AutoMatch] Button disappeared after settle, re-polling...");
scheduleManagedTimeout(poll, self.POLL_INTERVAL_MS);
}
});
} else {
scheduleManagedTimeout(poll, self.POLL_INTERVAL_MS);
}
}
poll();
});
}).then(function (found) {
if (!found) {
self.inProgress = false;
if (self.attemptCount < self.MAX_ATTEMPTS) {
scheduleManagedTimeout(function () { self.try(); }, self.ACTION_DELAY_MS);
} else {
self.attemptCount = 0;
}
return;
}
if (found.type === "decline") {
self._clickElement(found.el, "Decline Rematch");
log("[AutoMatch] Declined rematch, will look for New Game next...");
self.inProgress = false;
scheduleManagedTimeout(function () { self.try(); }, 3000);
return;
}
_suppressNewGameActionUntil = Date.now() + 1500;
self._clickElement(found.el, "New Game (" + found.type + ")");
if (typeof handleNewGame === "function") handleNewGame();
scheduleManagedTimeout(function () {
if (!State.autoRun && State.autoMatch) {
saveSetting("autoRun", true);
syncToggleUI("btn-auto-run", true);
log("[AutoMatch] Auto Run enabled after new game");
}
}, 3000);
self.inProgress = false;
self.attemptCount = 0;
log("[AutoMatch] New game started!");
}).catch(function (e) {
err("[AutoMatch] Error:", e);
self.inProgress = false;
});
}
};
// =====================================================
// Section 41: Panel State Management
// =====================================================
function applyPanelState(state) {
let panel = $("#chess-assist-panel");
if (!panel) return;
panel.classList.remove("minimized", "maximized", "closed");
if (state !== "maximized") panel.classList.add(state);
saveSetting("panelState", state);
if (state === "closed") {
if (UI && typeof UI.clearAll === "function") UI.clearAll();
}
}
// =====================================================
// Section 42: New Game Action Detection
// =====================================================
let newGameActionMouseDownHandler = function (e) {
if (Date.now() < _suppressNewGameActionUntil) return;
if (AutoMatch && typeof AutoMatch._isAllowedContext === "function" && !AutoMatch._isAllowedContext()) {
return;
}
if (e.button !== 0) return;
let target = e.target;
let btnText = (target.innerText || target.textContent || "").toLowerCase();
let dataCy = (target.getAttribute("data-cy") || "").toLowerCase();
let isActionButton = false;
let actionType = "";
if (dataCy.includes("new-game") || dataCy.includes("newgame")) {
isActionButton = true;
actionType = "new-game";
} else if (dataCy.includes("new-bot") || dataCy.includes("bot")) {
isActionButton = true;
actionType = "new-bot";
} else if (dataCy.includes("rematch")) {
isActionButton = true;
actionType = "rematch";
}
if (!isActionButton) {
if (btnText.includes("new game") ||
btnText.includes("baru") ||
btnText.includes("10 mnt") ||
btnText.includes("5 mnt") ||
btnText.includes("3 mnt") ||
btnText.includes("1 mnt") ||
btnText.includes("game baru")) {
isActionButton = true;
actionType = "new-game";
}
else if (btnText.includes("new bot") ||
btnText.includes("bot baru") ||
btnText.includes("play bot") ||
btnText.includes("main bot")) {
isActionButton = true;
actionType = "new-bot";
}
else if (btnText.includes("rematch") ||
btnText.includes("tanding ulang") ||
btnText.includes("main lagi")) {
isActionButton = true;
actionType = "rematch";
}
}
if (!isActionButton && target.closest) {
let parentButton = target.closest("button");
if (parentButton) {
let parentDataCy = (parentButton.getAttribute("data-cy") || "").toLowerCase();
if (parentDataCy.includes("new-game") ||
parentDataCy.includes("new-bot") ||
parentDataCy.includes("rematch")) {
isActionButton = true;
actionType = parentDataCy.includes("bot") ? "new-bot" :
parentDataCy.includes("rematch") ? "rematch" : "new-game";
}
}
}
if (isActionButton) {
let now = Date.now();
if (now - State.lastNewGameLogTs < 2000) return;
State.lastNewGameLogTs = now;
log("Action button detected:", actionType, "| Text:", btnText);
scheduleManagedTimeout(() => handleNewGame(), 500);
}
};
document.addEventListener("mousedown", newGameActionMouseDownHandler, true);
_eventListeners.push({ element: document, type: "mousedown", handler: newGameActionMouseDownHandler, options: true });
// =====================================================
// Section 43: Initialization and Main Loop
// =====================================================
function handleNewGame() {
log("[NewGame] Detected! Resetting all state");
State.statusInfo = "New game detected";
State.gameEnded = false;
State.moveExecutionInProgress = false;
resetResignState();
UI.clearAll();
CCTAnalyzer.clearCache();
ThreatDetectionSystem.clearCache();
MoveHistory.clear();
SmartPremove.resetExecutionTracking();
Engine.resetPremoveState();
Syzygy.clear();
State.lastAutoRunFen = null;
State._lastAnalysisFen = null;
State.mainPVLine = [];
State.analysisPVLine = [];
State.principalVariation = "";
State.premoveExecutedForFen = null;
State.premoveAnalysisInProgress = false;
State.premoveLastAnalysisTime = 0;
State.premoveRetryCount = 0;
State.premoveLiveChance = 0;
State.premoveTargetChance = clamp(State.premoveMinConfidence || 0, 0, 100);
State.premoveLastEvalDisplay = "-";
State.premoveLastMoveDisplay = "-";
State.premoveChanceReason = "Waiting for engine PV";
State.premoveChanceUpdatedTs = 0;
State.analysisHistoryCursor = 0;
State.analysisAcplFen = "";
State.analysisEvalText = "0.00";
State.analysisLastRecordedKey = "";
State._analysisAutoPlayApproved = false;
State._analysisAutoPlayMove = null;
if (Engine.main) Engine.main.postMessage("ucinewgame");
let openingDisp = $("#currentOpeningDisplay");
if (openingDisp) {
openingDisp.textContent = "Game Start";
openingDisp.style.color = "#1E90FF";
}
_OPENING_BOOK_CACHE = null;
_OPENING_NAMES_CACHE = null;
_openingBookVersion = 0;
State.statusInfo = "New Game Started";
UI.updateStatusInfo();
UI.updateSyzygyDisplay();
}
function startMainLoop() {
if (State.loopStarted) {
log("[MainLoop] Already started, skipping");
return;
}
State.loopStarted = true;
log("[MainLoop] Starting...");
UI.initPanicKey();
let clockLoop = function () {
if (!_allLoopsActive) return;
const startedAt = RuntimeGuard._nowMs();
try {
UI.updateClock();
if (UI && typeof UI._refreshEvalBarStatus === "function") UI._refreshEvalBarStatus();
} catch (e) { }
RuntimeGuard.trackLoop("clockLoop", startedAt);
scheduleManagedTimeout(clockLoop, 1000 + randomInt(-150, 150));
};
clockLoop();
let mainLoop = function () {
if (!_allLoopsActive) return;
const startedAt = RuntimeGuard._nowMs();
try {
UI.updateTurnLEDs();
if (State.analysisMode) {
if (State.gameEnded) State.gameEnded = false;
analysisCheck();
RuntimeGuard.trackLoop("mainLoop", startedAt);
scheduleNextMainLoop();
return;
}
if (State.gameEnded) {
RuntimeGuard.trackLoop("mainLoop", startedAt);
scheduleNextMainLoop();
return;
}
let myTurn = isPlayersTurn();
if (myTurn) {
if (State.autoRun && Math.random() > 0.08) {
autoRunCheck();
}
} else {
if (State.premoveEnabled && !State.premoveAnalysisInProgress) {
premoveCheck();
}
}
} catch (e) {
warn("[MainLoop] Error:", e);
}
RuntimeGuard.trackLoop("mainLoop", startedAt);
scheduleNextMainLoop();
};
let scheduleNextMainLoop = function () {
if (!_allLoopsActive) return;
let baseInterval = CONFIG.UPDATE_INTERVAL;
let jitter = (Math.random() - 0.5) * 60;
let nextInterval = Math.max(80, Math.floor(baseInterval + jitter));
scheduleManagedTimeout(mainLoop, nextInterval);
};
scheduleNextMainLoop();
let autoMatchAttempts = 0;
let autoMatchLoop = function () {
if (!_allLoopsActive) return;
const startedAt = RuntimeGuard._nowMs();
try {
autoMatchCheck();
} catch (e) { }
RuntimeGuard.trackLoop("autoMatchLoop", startedAt);
let baseDelay = Math.min(3000 + (autoMatchAttempts * 200), 8000);
let nextCheck = baseDelay + randomInt(-400, 400);
autoMatchAttempts = (autoMatchAttempts + 1) % 10;
scheduleManagedTimeout(autoMatchLoop, nextCheck);
};
autoMatchLoop();
let gameOverLoop = function () {
if (!_allLoopsActive) return;
const startedAt = RuntimeGuard._nowMs();
try {
if (State.analysisMode) {
if (State.gameEnded) State.gameEnded = false;
RuntimeGuard.trackLoop("gameOverLoop", startedAt);
scheduleManagedTimeout(gameOverLoop, 1500 + randomInt(-250, 250));
return;
}
let isGameOver = false;
let endReason = "";
let gameOverSelectors = [
".game-over-modal-shell-container",
".game-over-modal-container",
"[data-cy='game-over-modal-content']",
".game-over-modal-shell-content",
"[data-cy='game-over-header']",
".game-over-modal-header-component",
"[data-cy='game-over-ad-container']",
".game-over-modal-shell-buttons",
"[data-cy='game-over-new-game-button']"
];
for (let i = 0; i < gameOverSelectors.length; i++) {
if ($(gameOverSelectors[i])) {
isGameOver = true;
endReason = "DOM";
break;
}
}
if (!isGameOver) {
let fen = getAccurateFen();
if (fen) {
let game = getGame();
if (game && typeof game.isGameOver === 'function' && game.isGameOver()) {
isGameOver = true;
endReason = "API";
}
}
}
if (isGameOver && !State.gameEnded) {
log("[GameOver] Detected:", endReason);
State.statusInfo = "Game ended: " + endReason;
State.gameEnded = true;
saveSetting("autoRun", false);
syncToggleUI("btn-auto-run", false);
UI.clearAll();
UI._removeAllVisuals();
if (State.autoMatch) {
scheduleManagedTimeout(function () {
if (_allLoopsActive) AutoMatch.try();
}, 2000 + randomInt(0, 1000));
}
}
} catch (e) { }
RuntimeGuard.trackLoop("gameOverLoop", startedAt);
scheduleManagedTimeout(gameOverLoop, 1500 + randomInt(-250, 250));
};
gameOverLoop();
let prevFen = "";
let fenPollLoop = function () {
if (!_allLoopsActive) return;
const startedAt = RuntimeGuard._nowMs();
try {
let fen = getAccurateFen();
if (!fen) {
RuntimeGuard.trackLoop("fenPollLoop", startedAt);
if (_allLoopsActive) scheduleManagedTimeout(fenPollLoop, CONFIG.FEN_POLL_INTERVAL);
return;
}
if (fen.indexOf("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR") === 0) {
if (prevFen && prevFen.indexOf("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR") !== 0) {
handleNewGame();
}
}
prevFen = fen;
Syzygy.maybeProbe(fen);
if (Math.random() < 0.1) {
applyAutoDepthFromOpponent();
updateMoveNumber(fen);
trimCaches();
}
} catch (e) { }
RuntimeGuard.trackLoop("fenPollLoop", startedAt);
if (_allLoopsActive) scheduleManagedTimeout(fenPollLoop, CONFIG.FEN_POLL_INTERVAL + randomInt(-30, 30));
};
fenPollLoop();
let runtimeWatchdogLoop = function () {
if (!_allLoopsActive) return;
const startedAt = RuntimeGuard._nowMs();
try {
RuntimeGuard.checkCachePressure();
RuntimeGuard.checkPremoveWatchdog();
RuntimeGuard.checkEngineWatchdog();
RuntimeGuard.checkUIWatchdog();
RuntimeGuard.logSoakSummary();
UI.updateDiagnosticsDisplay();
trimTimeoutIds();
} catch (e) {
warn("[Watchdog] Error:", e);
}
RuntimeGuard.trackLoop("runtimeWatchdogLoop", startedAt);
scheduleManagedTimeout(runtimeWatchdogLoop, 5000 + randomInt(-600, 600));
};
runtimeWatchdogLoop();
_premoveCacheClearInterval = setInterval(function () {
if (!_allLoopsActive) {
clearInterval(_premoveCacheClearInterval);
return;
}
try {
trimCaches();
} catch (e) { }
}, 30000);
}
function init() {
State.statusInfo = "Starting initialization...";
let loadPromise = isTampermonkey ? Promise.resolve(true) : loadStockfishManually();
loadPromise.then(function () {
return sleep(1000);
}).then(function () {
let attempts = 0;
let waitForBoard = function () {
if (getBoardElement() || attempts >= 30) return Promise.resolve();
attempts++;
return sleep(500).then(waitForBoard);
};
return waitForBoard();
}).then(function () {
return Engine.init();
}).then(function (engineOk) {
if (!engineOk) {
err("Engine failed to initialize");
return sleep(2000).then(function () {
return Engine.init();
});
}
return true;
}).then(function () {
createPanel();
let completeStartup = function () {
startMainLoop();
UI.updatePVDisplay();
State.statusInfo = "Ready";
UI.updateStatusInfo();
if (State.analysisMode) {
Engine.loadAnalysisEngine();
let analysisHistoryBody = $("#moveHistoryTableBody");
if (analysisHistoryBody) analysisHistoryBody.innerHTML = "";
State.analysisHistoryCursor = 0;
State.analysisAcplFen = "";
State.analysisEvalText = "0.00";
State.analysisLastRecordedKey = "";
syncAnalysisMoveHistory();
State._lastAnalysisFen = null;
analysisCheck();
}
log("Initialization complete!");
};
if (!State.onboardingAccepted) {
showWelcomeConsentModal(completeStartup);
} else {
completeStartup();
}
}).catch(function (e) {
err("Initialization error:", e);
});
}
function cleanupAll() {
_allLoopsActive = false;
clearManagedTimeouts();
State.moveExecutionInProgress = false;
cleanupEventListeners();
UI.clearAll();
UI._removeAllVisuals();
CCTAnalyzer.clearCache();
ThreatDetectionSystem.clearCache();
Syzygy.clear();
if (_premoveCacheClearInterval) {
clearInterval(_premoveCacheClearInterval);
_premoveCacheClearInterval = null;
}
if (pendingMoveTimeoutId) {
clearTimeout(pendingMoveTimeoutId);
pendingMoveTimeoutId = null;
}
if (_resignTimeout) {
clearTimeout(_resignTimeout);
_resignTimeout = null;
}
if (_resignObserver) {
_resignObserver.disconnect();
_resignObserver = null;
}
let welcomeOverlay = $("#cap-welcome-overlay");
if (welcomeOverlay && welcomeOverlay.parentNode) {
welcomeOverlay.parentNode.removeChild(welcomeOverlay);
}
Engine.terminate();
cancelPendingMove();
}
let _cleanupDone = false;
function runCleanupOnce() {
if (_cleanupDone) return;
_cleanupDone = true;
cleanupAll();
}
window.addEventListener("beforeunload", function () {
runCleanupOnce();
});
let _initStarted = false;
function startInitOnce() {
if (_initStarted) return;
_initStarted = true;
init();
}
if (document.readyState === "loading") {
let domReadyHandler = function () {
document.removeEventListener("DOMContentLoaded", domReadyHandler);
startInitOnce();
};
document.addEventListener("DOMContentLoaded", domReadyHandler);
} else {
scheduleManagedTimeout(startInitOnce, 500);
}
// =====================================================
// End of Chess.com Assistant Pro
// =====================================================
})();
/*
______
/ ____/___ ____ ___ ____ ____ ________ _____
/ / / __ / __ `__ / __ / __ / ___/ _ / ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ /
____/____/_/ /_/ /_/ .___/____/____/___/_/
/_/
version 1.2.0 2025-02-12 16:20:11
*/