Super chess Bot is a tournament level bullet bot
// ==UserScript==
// @name ♟Super-chess-Bot
// @namespace http://tampermonkey.net/
// @version 9.0.0
// @description Super chess Bot is a tournament level bullet bot
// @author quantavil
// @match https://www.chess.com/play/computer*
// @match https://www.chess.com/game/*
// @match https://www.chess.com/play/online*
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=chess.com
// @grant GM_xmlhttpRequest
// @connect stockfish.online
// @antifeature membership
// ==/UserScript==
(() => {
// src/utils.js
function debounce(fn, wait = 150) {
let t = null;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), wait);
};
}
function getRandomDepth(botPower) {
const minDepth = 5;
const maxDepth = Math.max(botPower || 10, minDepth);
return Math.floor(Math.random() * (maxDepth - minDepth + 1)) + minDepth;
}
function getHumanDelay(baseDelay, randomDelay) {
return baseDelay + Math.floor(Math.random() * randomDelay);
}
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
var qs = (sel, root = document) => root.querySelector(sel);
var qsa = (sel, root = document) => Array.from(root.querySelectorAll(sel));
async function waitForElement(selector, timeout = 15e3) {
return new Promise((resolve, reject) => {
const existing = qs(selector);
if (existing)
return resolve(existing);
let timeoutId;
const obs = new MutationObserver(() => {
const el = qs(selector);
if (el) {
clearTimeout(timeoutId);
obs.disconnect();
resolve(el);
}
});
obs.observe(document.body, { childList: true, subtree: true });
timeoutId = setTimeout(() => {
obs.disconnect();
reject(new Error(`Element ${selector} not found within ${timeout}ms`));
}, timeout);
});
}
function scoreFrom(obj) {
if (!obj)
return {};
if (typeof obj === "object") {
if ("mate" in obj && obj.mate !== 0)
return { mate: parseInt(obj.mate, 10) };
if ("cp" in obj)
return { cp: parseInt(obj.cp, 10) };
}
if (typeof obj === "string") {
if (obj.toUpperCase().includes("M")) {
const m = parseInt(obj.replace(/[^-0-9]/g, ""), 10);
if (!isNaN(m))
return { mate: m };
}
const cpFloat = parseFloat(obj);
if (!isNaN(cpFloat))
return { cp: Math.round(cpFloat * 100) };
}
if (typeof obj === "number")
return { cp: Math.round(obj * 100) };
return {};
}
function scoreToDisplay(score) {
if (score && typeof score.mate === "number" && score.mate !== 0)
return `M${score.mate}`;
if (score && typeof score.cp === "number")
return (score.cp / 100).toFixed(2);
return "-";
}
function scoreNumeric(s) {
if (!s)
return -Infinity;
if (typeof s.mate === "number")
return s.mate > 0 ? 1e5 - s.mate : -1e5 - s.mate;
if (typeof s.cp === "number")
return s.cp;
return -Infinity;
}
function fenCharAtSquare(fen, square) {
if (!fen || !square)
return null;
const placement = fen.split(" ")[0];
const ranks = placement.split("/");
const file = "abcdefgh".indexOf(square[0]);
const rankNum = parseInt(square[1], 10);
if (file < 0 || rankNum < 1 || rankNum > 8 || ranks.length !== 8)
return null;
const row = 8 - rankNum;
const rowStr = ranks[row];
let col = 0;
for (const ch of rowStr) {
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;
const isUpper = ch === ch.toUpperCase();
return { color: isUpper ? "w" : "b", type: ch.toLowerCase() };
}
// src/config.js
var API_URL = "https://stockfish.online/api/s/v2.php";
var MULTIPV = 1;
var ANALYZE_TIMEOUT_MS = 1e3;
var AUTO_MOVE_BASE = 200;
var AUTO_MOVE_STEP = 20;
var RANDOM_JITTER_MIN = 0;
var GAME_CACHE_TTL = 100;
// src/state.js
var BotState = {
hackEnabled: 0,
botPower: 8,
updateSpeed: 10,
autoMove: 1,
autoMoveSpeed: 10,
randomDelay: 50,
currentEvaluation: "-",
bestMove: "-",
principalVariation: "-",
statusInfo: "Ready",
premoveEnabled: 0,
premoveMode: "every",
premovePieces: { q: 1, r: 1, b: 1, n: 1, k: 1, p: 1 },
premoveChance: 85,
currentPremoveReasons: "",
autoRematch: 0
};
var LRUCache = class {
constructor(limit = 2e3) {
this.limit = limit;
this.cache = /* @__PURE__ */ new Map();
}
get(key) {
if (!this.cache.has(key))
return void 0;
const val = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, val);
return val;
}
set(key, value) {
if (this.cache.has(key))
this.cache.delete(key);
else if (this.cache.size >= this.limit) {
this.cache.delete(this.cache.keys().next().value);
}
this.cache.set(key, value);
}
clear() {
this.cache.clear();
}
};
var PositionCache = new LRUCache(2e3);
var Settings = {
save: debounce(() => {
try {
const menuWrap = qs("#menuWrap");
const settings = {
hackEnabled: BotState.hackEnabled,
botPower: BotState.botPower,
updateSpeed: BotState.updateSpeed,
autoMove: BotState.autoMove,
autoMoveSpeed: BotState.autoMoveSpeed,
randomDelay: Math.max(RANDOM_JITTER_MIN, BotState.randomDelay),
premoveEnabled: BotState.premoveEnabled,
premoveMode: BotState.premoveMode,
premovePieces: BotState.premovePieces,
autoRematch: BotState.autoRematch,
menuPosition: menuWrap ? { top: menuWrap.style.top, left: menuWrap.style.left } : null
};
localStorage.setItem("gabibot_settings", JSON.stringify(settings));
} catch (e) {
console.warn("Failed to save settings:", e);
}
}, 200),
load() {
try {
const saved = localStorage.getItem("gabibot_settings");
if (!saved)
return null;
const s = JSON.parse(saved);
BotState.hackEnabled = s.hackEnabled ?? 0;
BotState.botPower = s.botPower ?? 8;
BotState.updateSpeed = s.updateSpeed ?? 10;
BotState.autoMove = s.autoMove ?? 1;
BotState.autoMoveSpeed = s.autoMoveSpeed ?? 8;
BotState.randomDelay = Math.max(RANDOM_JITTER_MIN, s.randomDelay ?? 300);
BotState.premoveEnabled = s.premoveEnabled ?? 0;
BotState.premoveMode = s.premoveMode ?? "every";
BotState.premovePieces = s.premovePieces ?? { q: 1, r: 1, b: 1, n: 1, k: 1, p: 1 };
BotState.autoRematch = s.autoRematch ?? 0;
return s;
} catch (e) {
console.error("Failed to load settings:", e);
return null;
}
}
};
var cachedGame = null;
var cachedGameTimestamp = 0;
var cachedBoardFlipped = false;
var cachedFlipTimestamp = 0;
var getBoard = () => qs("chess-board") || qs(".board") || qs('[class*="board"]');
var getGame = () => {
const now = Date.now();
if (cachedGame && now - cachedGameTimestamp < GAME_CACHE_TTL) {
return cachedGame;
}
cachedGame = getBoard()?.game || null;
cachedGameTimestamp = now;
return cachedGame;
};
var getFen = (g) => {
try {
return g?.getFEN ? g.getFEN() : null;
} catch {
return null;
}
};
var getPlayerColor = (g) => {
try {
const v = g?.getPlayingAs?.();
return v === 2 ? "b" : "w";
} catch {
return "w";
}
};
var getSideToMove = (g) => {
const fen = getFen(g);
return fen ? fen.split(" ")[1] || null : null;
};
var isPlayersTurn = (g) => {
const me = getPlayerColor(g), stm = getSideToMove(g);
return !!me && !!stm && me === stm;
};
var pa = () => getGame()?.getPlayingAs ? getGame().getPlayingAs() : 1;
function isBoardFlipped() {
const now = Date.now();
if (now - cachedFlipTimestamp < 1e3)
return cachedBoardFlipped;
const el = getBoard();
let flipped = false;
try {
const attr = el?.getAttribute?.("orientation");
if (attr === "black")
flipped = true;
else if (attr === "white")
flipped = false;
else if (el?.classList?.contains("flipped"))
flipped = true;
else if (getGame()?.getPlayingAs?.() === 2)
flipped = true;
} catch {
}
cachedBoardFlipped = flipped;
cachedFlipTimestamp = now;
return flipped;
}
function invalidateGameCache() {
cachedGame = null;
cachedGameTimestamp = 0;
}
// src/board.js
var boardCtx = null;
var domObserver = null;
var pendingMoveTimeoutId = null;
function attachToBoard(boardEl) {
invalidateGameCache();
detachFromBoard();
if (!boardEl) {
console.warn("GabiBot: No board element to attach.");
return;
}
if (getComputedStyle(boardEl).position === "static")
boardEl.style.position = "relative";
const drawingBoard = document.createElement("canvas");
drawingBoard.id = "arrowCanvas";
drawingBoard.style.cssText = "position:absolute;top:0;left:0;pointer-events:none;z-index:100;";
const ctx = drawingBoard.getContext("2d");
const evalBarWrap = document.createElement("div");
evalBarWrap.id = "evaluationBarWrap";
const whiteBar = document.createElement("div");
whiteBar.id = "evaluationBarWhite";
const blackBar = document.createElement("div");
blackBar.id = "evaluationBarBlack";
evalBarWrap.appendChild(whiteBar);
evalBarWrap.appendChild(blackBar);
boardEl.appendChild(evalBarWrap);
boardEl.appendChild(drawingBoard);
const resizeCanvas = () => {
const rect = boardEl.getBoundingClientRect();
drawingBoard.width = rect.width;
drawingBoard.height = rect.height;
};
resizeCanvas();
const ro = new ResizeObserver(resizeCanvas);
ro.observe(boardEl);
const cancelPendingOnUserAction = () => {
if (pendingMoveTimeoutId) {
clearTimeout(pendingMoveTimeoutId);
pendingMoveTimeoutId = null;
BotState.statusInfo = "Manual move in progress...";
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
}
};
const touchOpts = { passive: true, capture: true };
boardEl.addEventListener("mousedown", cancelPendingOnUserAction, true);
boardEl.addEventListener("touchstart", cancelPendingOnUserAction, touchOpts);
boardCtx = {
boardEl,
drawingBoard,
ctx,
evalBarWrap,
resizeObserver: ro,
cancelPendingOnUserAction,
touchOpts,
detachListeners() {
try {
boardEl.removeEventListener("mousedown", cancelPendingOnUserAction, true);
} catch {
}
try {
boardEl.removeEventListener("touchstart", cancelPendingOnUserAction, touchOpts);
} catch {
}
try {
ro.disconnect();
} catch {
}
try {
drawingBoard.remove();
} catch {
}
try {
evalBarWrap.remove();
} catch {
}
}
};
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
}
function detachFromBoard() {
if (!boardCtx)
return;
try {
boardCtx.detachListeners();
} catch {
}
boardCtx = null;
}
function startDomBoardWatcher() {
if (domObserver)
try {
domObserver.disconnect();
} catch {
}
domObserver = new MutationObserver(debounce(() => {
const newBoard = qs("chess-board") || qs(".board") || qs('[class*="board"]');
if (!newBoard)
return;
if (!boardCtx || boardCtx.boardEl !== newBoard) {
console.log("GabiBot: Board element changed, re-attaching.");
attachToBoard(newBoard);
}
}, 200));
domObserver.observe(document.body, { childList: true, subtree: true });
}
function clearArrows() {
if (!boardCtx)
return;
const { drawingBoard, ctx } = boardCtx;
ctx.clearRect(0, 0, drawingBoard.width, drawingBoard.height);
}
function getSquareCenterClientXY(square) {
if (!boardCtx || !square || square.length < 2)
return null;
const file = "abcdefgh".indexOf(square[0]);
const rank = parseInt(square[1], 10);
if (file < 0 || isNaN(rank))
return null;
const el = boardCtx.boardEl;
const rect = el.getBoundingClientRect();
const size = Math.min(rect.width, rect.height);
const tile = size / 8;
const offsetX = rect.left + (rect.width - size) / 2;
const offsetY = rect.top + (rect.height - size) / 2;
let x = file, y = 8 - rank;
if (isBoardFlipped()) {
x = 7 - x;
y = 7 - y;
}
return { x: offsetX + (x + 0.5) * tile, y: offsetY + (y + 0.5) * tile };
}
function getSquareCenterCanvasXY(square) {
if (!boardCtx || !square || square.length < 2)
return null;
const p = getSquareCenterClientXY(square);
if (!p)
return null;
const rect = boardCtx.boardEl.getBoundingClientRect();
return { x: p.x - rect.left, y: p.y - rect.top };
}
function drawArrow(uciFrom, uciTo, color, thickness) {
if (!boardCtx || !uciFrom || !uciTo || uciFrom.length < 2 || uciTo.length < 2)
return;
const { drawingBoard, ctx } = boardCtx;
const a = getSquareCenterCanvasXY(uciFrom);
const b = getSquareCenterCanvasXY(uciTo);
if (!a || !b)
return;
const size = Math.min(drawingBoard.width, drawingBoard.height);
const tile = size / 8;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.lineWidth = thickness;
ctx.strokeStyle = color;
ctx.lineCap = "round";
ctx.stroke();
ctx.beginPath();
ctx.arc(a.x, a.y, tile / 7, 0, 2 * Math.PI);
ctx.fillStyle = color.replace("0.7", "0.3");
ctx.fill();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.arc(b.x, b.y, tile / 5, 0, 2 * Math.PI);
ctx.fillStyle = color.replace("0.7", "0.5");
ctx.fill();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
}
function dispatchPointerOrMouse(el, type, opts, usePointer) {
if (!el)
return;
if (usePointer) {
try {
el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true, composed: true, ...opts }));
return;
} catch {
}
}
el.dispatchEvent(new MouseEvent(type.replace("pointer", "mouse"), { bubbles: true, cancelable: true, composed: true, ...opts }));
}
function getTargetAt(x, y) {
return document.elementFromPoint(x, y) || boardCtx?.boardEl || document.body;
}
async function simulateClickMove(from, to) {
const a = getSquareCenterClientXY(from), b = getSquareCenterClientXY(to);
if (!a || !b)
return false;
const usePointer = !!window.PointerEvent;
const startEl = getTargetAt(a.x, a.y);
const endEl = getTargetAt(b.x, b.y);
const downStart = { clientX: a.x, clientY: a.y, pointerId: 1, pointerType: "mouse", isPrimary: true, buttons: 1 };
const upStart = { clientX: a.x, clientY: a.y, pointerId: 1, pointerType: "mouse", isPrimary: true, buttons: 0 };
const downEnd = { clientX: b.x, clientY: b.y, pointerId: 1, pointerType: "mouse", isPrimary: true, buttons: 1 };
const upEnd = { clientX: b.x, clientY: b.y, pointerId: 1, pointerType: "mouse", isPrimary: true, buttons: 0 };
dispatchPointerOrMouse(startEl, usePointer ? "pointerdown" : "mousedown", downStart, usePointer);
await sleep(2);
dispatchPointerOrMouse(startEl, usePointer ? "pointerup" : "mouseup", upStart, usePointer);
startEl.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, composed: true, clientX: a.x, clientY: a.y }));
await sleep(4);
dispatchPointerOrMouse(endEl, usePointer ? "pointerdown" : "mousedown", downEnd, usePointer);
await sleep(2);
dispatchPointerOrMouse(endEl, usePointer ? "pointerup" : "mouseup", upEnd, usePointer);
endEl.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, composed: true, clientX: b.x, clientY: b.y }));
return true;
}
async function waitForFenChange(prevFen, timeout = 1e3) {
return new Promise((resolve) => {
const start = performance.now();
const check = () => {
const g = getGame();
const fen = g?.getFEN ? g.getFEN() : null;
if (fen && fen !== prevFen)
return resolve(true);
if (performance.now() - start > timeout)
return resolve(false);
requestAnimationFrame(check);
};
requestAnimationFrame(check);
});
}
async function maybeSelectPromotion(prefer = "q") {
const preferList = (prefer ? [prefer] : ["q", "r", "b", "n"]).map((c) => c.toLowerCase());
const getCandidates = () => qsa('[data-test-element*="promotion"], [class*="promotion"] [class*="piece"], [class*="promotion"] button, .promotion-piece');
const tryClick = (el) => {
try {
el.click?.();
el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
return true;
} catch {
return false;
}
};
const start = Date.now();
while (Date.now() - start < 1e3) {
const nodes = getCandidates();
if (nodes.length) {
for (const pref of preferList) {
const match = nodes.find(
(n) => (n.dataset?.piece?.toLowerCase?.() || "") === pref || (n.getAttribute?.("data-piece") || "").toLowerCase() === pref || (n.getAttribute?.("aria-label") || "").toLowerCase().includes(pref) || (n.className || "").toLowerCase().includes(pref) || (n.textContent || "").toLowerCase().includes(pref)
);
if (match && tryClick(match))
return true;
}
if (tryClick(nodes[0]))
return true;
}
await sleep(60);
}
return false;
}
function cancelPendingMove() {
if (pendingMoveTimeoutId) {
clearTimeout(pendingMoveTimeoutId);
pendingMoveTimeoutId = null;
}
}
async function makeMove(from, to, expectedFen, promotionChar) {
const game = getGame();
if (!game || !BotState.autoMove)
return false;
const beforeFen = getFen(game);
if (!beforeFen || beforeFen !== expectedFen || !isPlayersTurn(game))
return false;
await simulateClickMove(from, to);
if (promotionChar)
await maybeSelectPromotion(String(promotionChar).toLowerCase());
const changed = await waitForFenChange(beforeFen, 400);
return !!changed;
}
function executeMove(from, to, analysisFen, promotionChar, tickCallback) {
if (BotState.hackEnabled && BotState.autoMove) {
const game = getGame();
if (!game || !isPlayersTurn(game)) {
BotState.statusInfo = "Waiting for opponent...";
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
return;
}
cancelPendingMove();
const baseDelay = Math.max(0, AUTO_MOVE_BASE - BotState.autoMoveSpeed * AUTO_MOVE_STEP);
const totalDelay = getHumanDelay(baseDelay, BotState.randomDelay);
console.log(`GabiBot: Delay ${totalDelay}ms`);
BotState.statusInfo = `Moving in ${(totalDelay / 1e3).toFixed(1)}s`;
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
pendingMoveTimeoutId = setTimeout(async () => {
const g = getGame();
if (!g)
return;
if (!isPlayersTurn(g)) {
BotState.statusInfo = "Move canceled (not our turn)";
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
return;
}
if (getFen(g) !== analysisFen) {
BotState.statusInfo = "Move canceled (position changed)";
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
return;
}
BotState.statusInfo = "Making move...";
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
const success = await makeMove(from, to, analysisFen, promotionChar);
BotState.statusInfo = success ? "\u2713 Move made!" : "\u274C Move failed";
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
if (!success) {
setTimeout(() => {
if (BotState.hackEnabled && isPlayersTurn(getGame())) {
if (tickCallback)
tickCallback();
}
}, 250);
}
}, totalDelay);
} else {
BotState.statusInfo = "Ready (manual)";
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
}
}
function updateEvaluationBar(evaluation, playingAs) {
if (!boardCtx)
return;
const whiteBar = boardCtx.evalBarWrap.querySelector("#evaluationBarWhite");
const blackBar = boardCtx.evalBarWrap.querySelector("#evaluationBarBlack");
if (!whiteBar || !blackBar)
return;
let score = 0;
if (typeof evaluation === "string") {
if (evaluation === "-" || evaluation === "Error") {
whiteBar.style.height = "50%";
blackBar.style.height = "50%";
return;
}
if (evaluation.includes("M")) {
const m = parseInt(evaluation.replace("M", "").replace("+", ""), 10);
score = m > 0 ? 10 : -10;
} else {
score = parseFloat(evaluation);
}
} else {
score = parseFloat(evaluation);
}
if (isNaN(score)) {
whiteBar.style.height = "50%";
blackBar.style.height = "50%";
return;
}
const maxScore = 5;
const clampedScore = Math.max(-maxScore, Math.min(maxScore, score));
const whitePercent = 50 + clampedScore / maxScore * 50;
const blackPercent = 100 - whitePercent;
whiteBar.style.height = `${whitePercent}%`;
blackBar.style.height = `${blackPercent}%`;
const ourColor = getPlayerColor(getGame());
const ourEval = ourColor === "w" ? score : -score;
if (ourEval < -2) {
boardCtx.evalBarWrap.style.borderColor = "rgba(255, 100, 100, 0.5)";
} else if (ourEval > 2) {
boardCtx.evalBarWrap.style.borderColor = "rgba(100, 255, 100, 0.5)";
} else {
boardCtx.evalBarWrap.style.borderColor = "rgba(255, 255, 255, 0.2)";
}
}
// src/ui.js
var ui = {
menuWrap: null,
setText(name, value, title) {
if (!this.menuWrap)
return;
const el = this.menuWrap.querySelector(`[name="${name}"]`);
if (!el)
return;
const state = el.querySelector(".itemState") || el.children[el.children.length - 1];
if (state) {
state.textContent = value ?? "-";
if (title)
state.title = title;
}
},
updateDisplay(playingAs) {
this.setText("currentEvaluation", BotState.currentEvaluation);
this.setText("bestMove", BotState.bestMove);
this.setText("pvDisplay", BotState.principalVariation, BotState.principalVariation);
this.setText("statusInfo", BotState.statusInfo);
const chanceEl = this.menuWrap?.querySelector('[name="premoveChance"] .itemState');
if (chanceEl && BotState.currentPremoveChance !== void 0) {
const pct = `${Math.round(BotState.currentPremoveChance)}%`;
const reasons = BotState.currentPremoveReasons;
chanceEl.textContent = reasons ? `${pct} (${reasons})` : pct;
chanceEl.title = reasons || "Premove confidence";
}
updateEvaluationBar(BotState.currentEvaluation, playingAs);
},
Settings
};
function buildUI() {
const menuWrap = document.createElement("div");
menuWrap.id = "menuWrap";
const menuWrapStyle = document.createElement("style");
menuWrap.innerHTML = `
<div id="topText">
<a id="modTitle">\u265F GabiBot</a>
<button id="minimizeBtn" title="Minimize (Ctrl+B)">\u2500</button>
</div>
<div id="itemsList">
<div name="enableHack" class="listItem">
<input class="checkboxMod" type="checkbox">
<a class="itemDescription">Enable Bot</a>
<a class="itemState">Off</a>
</div>
<div name="autoMove" class="listItem">
<input class="checkboxMod" type="checkbox">
<a class="itemDescription">Auto Move</a>
<a class="itemState">Off</a>
</div>
<div class="divider"></div>
<div name="premoveEnabled" class="listItem">
<input class="checkboxMod" type="checkbox">
<a class="itemDescription">Premove System</a>
<a class="itemState">Off</a>
</div>
<div name="premoveMode" class="listItem select-row">
<a class="itemDescription">Premove Mode</a>
<select class="selectMod">
<option value="every">Every next move</option>
<option value="capture">Only if capture</option>
<option value="filter">By piece filters</option>
</select>
</div>
<div name="premoveChance" class="listItem info-item">
<a class="itemDescription">Premove Chance:</a>
<a class="itemState">0%</a>
</div>
<div name="premovePieces" class="listItem">
<div class="pieceFilters">
<label class="chip"><input type="checkbox" data-piece="q" checked><span>Q</span></label>
<label class="chip"><input type="checkbox" data-piece="r" checked><span>R</span></label>
<label class="chip"><input type="checkbox" data-piece="b" checked><span>B</span></label>
<label class="chip"><input type="checkbox" data-piece="n" checked><span>N</span></label>
<label class="chip"><input type="checkbox" data-piece="k" checked><span>K</span></label>
<label class="chip"><input type="checkbox" data-piece="p" checked><span>P</span></label>
</div>
<a class="itemDescription">Pieces</a>
<a class="itemState">-</a>
</div>
<div class="divider"></div>
<div name="autoRematch" class="listItem">
<input class="checkboxMod" type="checkbox">
<a class="itemDescription">Auto Rematch</a>
<a class="itemState">Off</a>
</div>
<div class="divider"></div>
<div name="botPower" class="listItem">
<input min="1" max="15" value="10" class="rangeSlider" type="range">
<a class="itemDescription">Depth</a>
<a class="itemState">12</a>
</div>
<div name="autoMoveSpeed" class="listItem">
<input min="1" max="10" value="8" class="rangeSlider" type="range">
<a class="itemDescription">Move Speed</a>
<a class="itemState">4</a>
</div>
<div name="randomDelay" class="listItem">
<input min="120" max="2000" value="300" class="rangeSlider" type="range">
<a class="itemDescription">Random Delay (ms)</a>
<a class="itemState">1000</a>
</div>
<div name="updateSpeed" class="listItem">
<input min="1" max="10" value="8" class="rangeSlider" type="range">
<a class="itemDescription">Update Rate</a>
<a class="itemState">8</a>
</div>
<div class="divider"></div>
<div name="currentEvaluation" class="listItem info-item">
<a class="itemDescription">Eval:</a>
<a class="itemState eval-value">-</a>
</div>
<div name="bestMove" class="listItem info-item">
<a class="itemDescription">Best:</a>
<a class="itemState">-</a>
</div>
<div name="pvDisplay" class="listItem info-item">
<a class="itemDescription">PV:</a>
<a class="itemState pv-text-state" title="Principal Variation">-</a>
</div>
<div name="statusInfo" class="listItem info-item">
<a class="itemDescription">Status:</a>
<a class="itemState status-text">Ready</a>
</div>
</div>
`;
menuWrapStyle.innerHTML = `
#menuWrap {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
border-radius: 8px;
z-index: 9999999;
display: grid;
grid-template-rows: auto 1fr;
width: 300px; max-height: 550px;
position: fixed;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(20, 20, 20, 0.95);
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
user-select: none;
top: 20px; right: 20px;
transition: opacity 0.3s ease, transform 0.3s ease;
}
#menuWrap.minimized { grid-template-rows: auto 0fr; max-height: 50px; }
#menuWrap.minimized #itemsList { overflow: hidden; opacity: 0; }
#menuWrap.grabbing { cursor: grabbing !important; opacity: 0.9; }
.divider { height: 1px; background: rgba(255, 255, 255, 0.1); margin: 10px 0; }
.pv-text-state { color: #aaa !important; font-size: 11px; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.eval-value { font-weight: bold; font-size: 14px; }
.status-text { color: #4CAF50 !important; font-size: 11px; }
.info-item { opacity: 0.8; margin-bottom: 8px !important; }
#evaluationBarWrap {
position: absolute;
height: 100%;
width: 20px;
left: -28px;
top: 0;
background: #000;
z-index: 50;
border-radius: 6px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.2);
}
#evaluationBarWhite { position: absolute; top: 0; left: 0; right: 0; background: #f0d9b5; transition: height 0.3s ease; }
#evaluationBarBlack { position: absolute; bottom: 0; left: 0; right: 0; background: #000; transition: height 0.3s ease; }
#topText { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px;
background: rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.1); cursor: move; }
#modTitle { color: #fff; font-size: 16px; font-weight: 600; letter-spacing: 0.5px; }
#minimizeBtn { background: rgba(255, 255, 255, 0.1); border: none; color: #fff; width: 24px; height: 24px;
border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.2s; }
#minimizeBtn:hover { background: rgba(255, 255, 255, 0.2); }
#itemsList { overflow-y: auto; overflow-x: hidden; padding: 12px 16px; transition: opacity 0.3s ease; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); }
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.3); }
.listItem { display: flex; align-items: center; margin-bottom: 12px; gap: 8px; }
.listItem.select-row { display: grid; grid-template-columns: 1fr 1.2fr; gap: 10px; align-items: center; }
.listItem.select-row .itemDescription { color: rgba(255, 255, 255, 0.85); font-weight: 500; }
.checkboxMod { appearance: none; width: 18px; height: 18px; border: 2px solid rgba(255, 255, 255, 0.3); border-radius: 4px;
background: rgba(255, 255, 255, 0.05); cursor: pointer; position: relative; transition: all 0.2s; flex-shrink: 0; }
.checkboxMod:checked { background: #4CAF50; border-color: #4CAF50; }
.checkboxMod:checked::after { content: "\u2713"; position: absolute; color: white; font-size: 12px; top: 50%; left: 50%; transform: translate(-50%, -50%); }
.rangeSlider { -webkit-appearance: none; flex: 1; height: 4px; border-radius: 2px; background: rgba(255, 255, 255, 0.1); outline: none; }
.rangeSlider::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: #4CAF50; cursor: pointer; transition: transform 0.2s; }
.rangeSlider::-webkit-slider-thumb:hover { transform: scale(1.2); }
.itemDescription { color: rgba(255, 255, 255, 0.7); font-size: 12px; flex: 1; }
.itemState { color: #fff; font-size: 12px; min-width: 35px; text-align: right; font-weight: 500; }
#arrowCanvas { position: absolute !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; pointer-events: none !important; z-index: 100 !important; }
.selectMod {
appearance: none;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.2);
color: #fff;
border-radius: 6px;
padding: 6px 28px 6px 10px;
flex: 1;
outline: none;
cursor: pointer;
font-size: 12px;
font-family: inherit;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23fff' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
transition: all 0.2s ease;
}
.selectMod:hover { background-color: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.3); }
.selectMod:focus { background-color: rgba(255,255,255,0.1); border-color: #4CAF50; box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); }
.selectMod option { background: #1a1a1a; color: #fff; padding: 8px; }
.pieceFilters { display: flex; flex-wrap: wrap; gap: 6px; }
.pieceFilters .chip {
user-select: none; display: inline-flex; align-items: center; gap: 6px;
padding: 5px 10px; background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.2); border-radius: 999px; cursor: pointer;
color: rgba(255,255,255,0.7); transition: all 0.2s ease;
font-size: 11px; font-weight: 500;
}
.pieceFilters .chip:hover { background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.3); }
.pieceFilters .chip input { appearance: none; width: 14px; height: 14px; border-radius: 3px; border: 2px solid rgba(255,255,255,0.4); background: rgba(255,255,255,0.05); transition: all 0.2s ease; }
.pieceFilters .chip input:checked { background: #4CAF50; border-color: #4CAF50; }
.pieceFilters .chip input:checked::after { content: "\u2713"; color: white; font-size: 9px; display: flex; align-items: center; justify-content: center; height: 100%; }
.pieceFilters .chip input:checked + span { color: #fff; font-weight: 600; }
`;
document.body.appendChild(menuWrap);
document.body.appendChild(menuWrapStyle);
ui.menuWrap = menuWrap;
const saved = Settings.load();
if (saved?.menuPosition) {
menuWrap.style.top = saved.menuPosition.top || "20px";
menuWrap.style.left = saved.menuPosition.left || "";
menuWrap.style.right = saved.menuPosition.left ? "auto" : "20px";
}
const getElementByName = (name, el) => el.querySelector(`[name="${name}"]`);
const getInputElement = (el) => el.children[0];
const getStateElement = (el) => el.children[el.children.length - 1];
function bindControl(name, type, variable) {
const modElement = getElementByName(name, menuWrap);
if (!modElement)
return;
const modState = getStateElement(modElement);
const modInput = getInputElement(modElement);
const key = variable.replace("BotState.", "");
if (type === "checkbox") {
modInput.checked = !!BotState[key];
modState.textContent = BotState[key] ? "On" : "Off";
modInput.addEventListener("input", () => {
BotState[key] = modInput.checked ? 1 : 0;
modState.textContent = BotState[key] ? "On" : "Off";
Settings.save();
});
} else if (type === "range") {
modInput.value = BotState[key];
modState.textContent = BotState[key];
modInput.addEventListener("input", () => {
let value = parseInt(modInput.value, 10);
const min = parseInt(modInput.min, 10);
const max = parseInt(modInput.max, 10);
value = Math.max(min, Math.min(max, value));
BotState[key] = value;
modInput.value = value;
modState.textContent = value;
Settings.save();
});
}
}
function bindSelect(name, variable) {
const el = getElementByName(name, menuWrap);
if (!el)
return;
const select = el.querySelector("select");
const key = variable.replace("BotState.", "");
select.value = BotState[key];
select.addEventListener("change", () => {
BotState[key] = select.value;
refreshPremoveUIVisibility();
Settings.save();
});
}
function bindPieceFilters() {
const el = getElementByName("premovePieces", menuWrap);
if (!el)
return;
const checks = qsa('.pieceFilters input[type="checkbox"]', el);
checks.forEach((chk) => {
const p = String(chk.dataset.piece || "").toLowerCase();
chk.checked = !!BotState.premovePieces[p];
});
checks.forEach((chk) => {
chk.addEventListener("input", () => {
const p = String(chk.dataset.piece || "").toLowerCase();
BotState.premovePieces[p] = chk.checked ? 1 : 0;
Settings.save();
});
});
}
function refreshPremoveUIVisibility() {
const row = getElementByName("premovePieces", menuWrap);
if (row)
row.style.display = BotState.premoveMode === "filter" ? "flex" : "none";
}
bindControl("enableHack", "checkbox", "BotState.hackEnabled");
bindControl("autoMove", "checkbox", "BotState.autoMove");
bindControl("botPower", "range", "BotState.botPower");
bindControl("autoMoveSpeed", "range", "BotState.autoMoveSpeed");
bindControl("updateSpeed", "range", "BotState.updateSpeed");
bindControl("randomDelay", "range", "BotState.randomDelay");
bindControl("premoveEnabled", "checkbox", "BotState.premoveEnabled");
bindSelect("premoveMode", "BotState.premoveMode");
bindPieceFilters();
refreshPremoveUIVisibility();
bindControl("autoRematch", "checkbox", "BotState.autoRematch");
makePanelDraggable(menuWrap);
document.getElementById("minimizeBtn").addEventListener("click", () => menuWrap.classList.toggle("minimized"));
document.addEventListener("keydown", (e) => {
if (e.key === "b" && e.ctrlKey) {
e.preventDefault();
menuWrap.classList.toggle("minimized");
}
});
}
function makePanelDraggable(panel) {
function clampToViewport() {
const rect = panel.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
const margin = 8;
panel.style.right = "auto";
let left = parseFloat(panel.style.left || rect.left);
let top = parseFloat(panel.style.top || rect.top);
left = Math.max(margin, Math.min(left, vw - rect.width - margin));
top = Math.max(margin, Math.min(top, vh - rect.height - margin));
panel.style.left = left + "px";
panel.style.top = top + "px";
}
function allowDragFromTarget(target, e) {
if (e.altKey)
return true;
const rect = panel.getBoundingClientRect();
const m = 14;
const nearEdge = e.clientX <= rect.left + m || e.clientX >= rect.right - m || e.clientY <= rect.top + m || e.clientY >= rect.bottom - m;
if (nearEdge)
return true;
if (target.closest("input, select, textarea, button, label, a"))
return false;
return true;
}
function startDrag(e) {
e.preventDefault();
const startRect = panel.getBoundingClientRect();
panel.classList.add("grabbing");
panel.style.right = "auto";
panel.style.left = startRect.left + "px";
panel.style.top = startRect.top + "px";
const startX = e.clientX;
const startY = e.clientY;
const move = (ev) => {
const dx = ev.clientX - startX;
const dy = ev.clientY - startY;
const vw = window.innerWidth;
const vh = window.innerHeight;
let newLeft = startRect.left + dx;
let newTop = startRect.top + dy;
const margin = 8;
const maxLeft = Math.max(margin, vw - startRect.width - margin);
const maxTop = Math.max(margin, vh - startRect.height - margin);
newLeft = Math.min(Math.max(newLeft, margin), maxLeft);
newTop = Math.min(Math.max(newTop, margin), maxTop);
panel.style.left = newLeft + "px";
panel.style.top = newTop + "px";
};
const up = () => {
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", up);
panel.classList.remove("grabbing");
try {
Settings.save();
} catch {
}
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", up);
}
panel.addEventListener("mousedown", (e) => {
if (e.button !== 0)
return;
if (!allowDragFromTarget(e.target, e))
return;
startDrag(e);
});
window.addEventListener("resize", clampToViewport);
setTimeout(clampToViewport, 50);
}
// src/engine.js
var currentAnalysisId = 0;
var currentAbortController = null;
var analysisRunning = false;
var lastFenProcessedMain = "";
var lastFenProcessedPremove = "";
var lastPremoveFen = "";
var lastPremoveUci = "";
function getLastFenProcessedMain() {
return lastFenProcessedMain;
}
function setLastFenProcessedMain(fen) {
lastFenProcessedMain = fen;
}
function getLastFenProcessedPremove() {
return lastFenProcessedPremove;
}
function setLastFenProcessedPremove(fen) {
lastFenProcessedPremove = fen;
}
function getLastPremoveFen() {
return lastPremoveFen;
}
function setLastPremoveFen(fen) {
lastPremoveFen = fen;
}
function getLastPremoveUci() {
return lastPremoveUci;
}
function setLastPremoveUci(uci) {
lastPremoveUci = uci;
}
var WP = 1;
var WN = 2;
var WB = 3;
var WR = 4;
var WQ = 5;
var WK = 6;
var BP = -1;
var BN = -2;
var BB = -3;
var BR = -4;
var BQ = -5;
var BK = -6;
var EMPTY = 0;
var FLAG_NONE = 0;
var FLAG_EP = 1;
var FLAG_CASTLE = 2;
var FLAG_PROMO = 4;
var MATE_SCORE = 3e4;
var TT_EXACT = 0;
var TT_ALPHA = 1;
var TT_BETA = 2;
var TT_SIZE = 65536;
var PST_PAWN = [
0,
0,
0,
0,
0,
0,
0,
0,
// rank 1
5,
10,
10,
-20,
-20,
10,
10,
5,
// rank 2
5,
-5,
-10,
0,
0,
-10,
-5,
5,
// rank 3
0,
0,
0,
20,
20,
0,
0,
0,
// rank 4
5,
5,
10,
25,
25,
10,
5,
5,
// rank 5
10,
10,
20,
30,
30,
20,
10,
10,
// rank 6
50,
50,
50,
50,
50,
50,
50,
50,
// rank 7
0,
0,
0,
0,
0,
0,
0,
0
// rank 8 (never has pawns)
];
var PST_KNIGHT = [
-50,
-40,
-30,
-30,
-30,
-30,
-40,
-50,
-40,
-20,
0,
5,
5,
0,
-20,
-40,
-30,
5,
10,
15,
15,
10,
5,
-30,
-30,
0,
15,
20,
20,
15,
0,
-30,
-30,
5,
15,
20,
20,
15,
5,
-30,
-30,
0,
10,
15,
15,
10,
0,
-30,
-40,
-20,
0,
0,
0,
0,
-20,
-40,
-50,
-40,
-30,
-30,
-30,
-30,
-40,
-50
];
var PST_BISHOP = [
-20,
-10,
-10,
-10,
-10,
-10,
-10,
-20,
-10,
5,
0,
0,
0,
0,
5,
-10,
-10,
10,
10,
10,
10,
10,
10,
-10,
-10,
0,
10,
10,
10,
10,
0,
-10,
-10,
5,
5,
10,
10,
5,
5,
-10,
-10,
0,
5,
10,
10,
5,
0,
-10,
-10,
0,
0,
0,
0,
0,
0,
-10,
-20,
-10,
-10,
-10,
-10,
-10,
-10,
-20
];
var PST_ROOK = [
0,
0,
0,
5,
5,
0,
0,
0,
-5,
0,
0,
0,
0,
0,
0,
-5,
-5,
0,
0,
0,
0,
0,
0,
-5,
-5,
0,
0,
0,
0,
0,
0,
-5,
-5,
0,
0,
0,
0,
0,
0,
-5,
-5,
0,
0,
0,
0,
0,
0,
-5,
5,
10,
10,
10,
10,
10,
10,
5,
0,
0,
0,
0,
0,
0,
0,
0
];
var PST_QUEEN = [
-20,
-10,
-10,
-5,
-5,
-10,
-10,
-20,
-10,
0,
5,
0,
0,
0,
0,
-10,
-10,
5,
5,
5,
5,
5,
0,
-10,
0,
0,
5,
5,
5,
5,
0,
-5,
-5,
0,
5,
5,
5,
5,
0,
-5,
-10,
0,
5,
5,
5,
5,
0,
-10,
-10,
0,
0,
0,
0,
0,
0,
-10,
-20,
-10,
-10,
-5,
-5,
-10,
-10,
-20
];
var PST_KING_MG = [
20,
30,
10,
0,
0,
10,
30,
20,
20,
20,
0,
0,
0,
0,
20,
20,
-10,
-20,
-20,
-20,
-20,
-20,
-20,
-10,
-20,
-30,
-30,
-40,
-40,
-30,
-30,
-20,
-30,
-40,
-40,
-50,
-50,
-40,
-40,
-30,
-30,
-40,
-40,
-50,
-50,
-40,
-40,
-30,
-30,
-40,
-40,
-50,
-50,
-40,
-40,
-30,
-30,
-40,
-40,
-50,
-50,
-40,
-40,
-30
];
var PST_KING_EG = [
-50,
-30,
-30,
-30,
-30,
-30,
-30,
-50,
-30,
-30,
0,
0,
0,
0,
-30,
-30,
-30,
-10,
20,
30,
30,
20,
-10,
-30,
-30,
-10,
30,
40,
40,
30,
-10,
-30,
-30,
-10,
30,
40,
40,
30,
-10,
-30,
-30,
-10,
20,
30,
30,
20,
-10,
-30,
-30,
-20,
-10,
0,
0,
-10,
-20,
-30,
-50,
-40,
-30,
-20,
-20,
-30,
-40,
-50
];
var PST = {
[WP]: PST_PAWN,
[WN]: PST_KNIGHT,
[WB]: PST_BISHOP,
[WR]: PST_ROOK,
[WQ]: PST_QUEEN
};
var PIECE_VAL = { 1: 100, 2: 320, 3: 330, 4: 500, 5: 900, 6: 0 };
function mirrorSq(sq) {
return (7 - (sq >> 3)) * 8 + (sq & 7);
}
function sqFile(sq) {
return sq & 7;
}
function sqRank(sq) {
return sq >> 3;
}
function sqName(sq) {
return "abcdefgh"[sqFile(sq)] + (sqRank(sq) + 1);
}
function nameToSq(s) {
return s.charCodeAt(0) - 97 + (s.charCodeAt(1) - 49) * 8;
}
var LocalEngine = class {
constructor() {
this.board = new Array(64).fill(EMPTY);
this.side = 1;
this.castling = 0;
this.epSquare = -1;
this.halfmove = 0;
this.fullmove = 1;
this.wKingSq = 4;
this.bKingSq = 60;
this.stateStack = [];
this.nodes = 0;
this.timeLimit = 0;
this.startTime = 0;
this.stopped = false;
this.pvTable = [];
this.killers = [];
this.history = new Int32Array(64 * 64);
this.tt = /* @__PURE__ */ new Map();
}
loadFen(fen) {
this.board.fill(EMPTY);
const parts = fen.split(" ");
const rows = parts[0].split("/");
const pieceMap = { p: BP, n: BN, b: BB, r: BR, q: BQ, k: BK, P: WP, N: WN, B: WB, R: WR, Q: WQ, K: WK };
for (let r = 0; r < 8; r++) {
let f = 0;
for (const ch of rows[7 - r]) {
if (ch >= "1" && ch <= "8") {
f += parseInt(ch);
} else {
const piece = pieceMap[ch] || EMPTY;
this.board[r * 8 + f] = piece;
if (piece === WK)
this.wKingSq = r * 8 + f;
else if (piece === BK)
this.bKingSq = r * 8 + f;
f++;
}
}
}
this.side = (parts[1] || "w") === "w" ? 1 : -1;
const c = parts[2] || "-";
this.castling = (c.includes("K") ? 1 : 0) | (c.includes("Q") ? 2 : 0) | (c.includes("k") ? 4 : 0) | (c.includes("q") ? 8 : 0);
this.epSquare = parts[3] && parts[3] !== "-" ? nameToSq(parts[3]) : -1;
this.halfmove = parseInt(parts[4]) || 0;
this.fullmove = parseInt(parts[5]) || 1;
this.stateStack = [];
}
toFen() {
let fen = "";
for (let r = 7; r >= 0; r--) {
let empty = 0;
for (let f = 0; f < 8; f++) {
const p = this.board[r * 8 + f];
if (p === EMPTY) {
empty++;
} else {
if (empty) {
fen += empty;
empty = 0;
}
const abs = Math.abs(p);
const ch = "xpnbrqk"[abs];
fen += p > 0 ? ch.toUpperCase() : ch;
}
}
if (empty)
fen += empty;
if (r > 0)
fen += "/";
}
let c = "";
if (this.castling & 1)
c += "K";
if (this.castling & 2)
c += "Q";
if (this.castling & 4)
c += "k";
if (this.castling & 8)
c += "q";
if (!c)
c = "-";
const ep = this.epSquare >= 0 ? sqName(this.epSquare) : "-";
return `${fen} ${this.side === 1 ? "w" : "b"} ${c} ${ep} ${this.halfmove} ${this.fullmove}`;
}
createMove(from, to, flags = FLAG_NONE, promo = EMPTY) {
return {
from,
to,
flags,
piece: this.board[from],
captured: flags & FLAG_EP ? -this.side : this.board[to],
promo
};
}
makeMove(mv) {
this.stateStack.push({
castling: this.castling,
epSquare: this.epSquare,
halfmove: this.halfmove,
fullmove: this.fullmove,
wKingSq: this.wKingSq,
bKingSq: this.bKingSq
});
const { from, to, flags, piece, promo } = mv;
const abs = Math.abs(piece);
this.board[from] = EMPTY;
this.board[to] = flags & FLAG_PROMO ? promo : piece;
if (abs === 6) {
if (this.side === 1)
this.wKingSq = to;
else
this.bKingSq = to;
}
if (flags & FLAG_EP) {
this.board[to - this.side * 8] = EMPTY;
}
if (flags & FLAG_CASTLE) {
if (to > from) {
this.board[from + 3] = EMPTY;
this.board[from + 1] = this.side * WR;
} else {
this.board[from - 4] = EMPTY;
this.board[from - 1] = this.side * WR;
}
}
if (abs === 1 && Math.abs(to - from) === 16) {
this.epSquare = from + to >> 1;
} else {
this.epSquare = -1;
}
if (abs === 6) {
if (this.side === 1)
this.castling &= ~3;
else
this.castling &= ~12;
}
if (from === 0 || to === 0)
this.castling &= ~2;
if (from === 7 || to === 7)
this.castling &= ~1;
if (from === 56 || to === 56)
this.castling &= ~8;
if (from === 63 || to === 63)
this.castling &= ~4;
this.halfmove = abs === 1 || mv.captured !== EMPTY ? 0 : this.halfmove + 1;
if (this.side === -1)
this.fullmove++;
this.side = -this.side;
}
unmakeMove(mv) {
this.side = -this.side;
const st = this.stateStack.pop();
this.castling = st.castling;
this.epSquare = st.epSquare;
this.halfmove = st.halfmove;
this.fullmove = st.fullmove;
this.wKingSq = st.wKingSq;
this.bKingSq = st.bKingSq;
const { from, to, flags, piece, captured, promo } = mv;
this.board[from] = piece;
this.board[to] = flags & FLAG_EP ? EMPTY : captured;
if (flags & FLAG_EP) {
this.board[to - this.side * 8] = -this.side;
}
if (flags & FLAG_CASTLE) {
if (to > from) {
this.board[from + 1] = EMPTY;
this.board[from + 3] = this.side * WR;
} else {
this.board[from - 1] = EMPTY;
this.board[from - 4] = this.side * WR;
}
}
}
findKingSq(side) {
return side === 1 ? this.wKingSq : this.bKingSq;
}
isAttacked(sq, bySide) {
const pawnDir = bySide === 1 ? 1 : -1;
const pawnRank = sqRank(sq) - pawnDir;
if (pawnRank >= 0 && pawnRank <= 7) {
const pf = sqFile(sq);
if (pf > 0 && this.board[pawnRank * 8 + pf - 1] === bySide * WP)
return true;
if (pf < 7 && this.board[pawnRank * 8 + pf + 1] === bySide * WP)
return true;
}
const kn = bySide * WN;
const knightOffsets = [-17, -15, -10, -6, 6, 10, 15, 17];
for (const off of knightOffsets) {
const t = sq + off;
if (t < 0 || t > 63)
continue;
if (Math.abs(sqFile(t) - sqFile(sq)) > 2)
continue;
if (this.board[t] === kn)
return true;
}
const kg = bySide * WK;
for (let dr = -1; dr <= 1; dr++) {
for (let df = -1; df <= 1; df++) {
if (!dr && !df)
continue;
const t = sq + dr * 8 + df;
if (t < 0 || t > 63 || Math.abs(sqFile(t) - sqFile(sq)) > 1)
continue;
if (this.board[t] === kg)
return true;
}
}
const sideR = bySide * WR, sideQ = bySide * WQ, sideB = bySide * WB;
const straightDirs = [8, -8, 1, -1];
const diagDirs = [9, 7, -9, -7];
for (const dir of straightDirs) {
let t = sq + dir;
while (t >= 0 && t <= 63) {
if (dir === 1 || dir === -1) {
if (sqRank(t) !== sqRank(t - dir))
break;
}
const p = this.board[t];
if (p !== EMPTY) {
if (p === sideR || p === sideQ)
return true;
break;
}
t += dir;
}
}
for (const dir of diagDirs) {
let t = sq + dir;
while (t >= 0 && t <= 63) {
if (Math.abs(sqFile(t) - sqFile(t - dir)) !== 1)
break;
const p = this.board[t];
if (p !== EMPTY) {
if (p === sideB || p === sideQ)
return true;
break;
}
t += dir;
}
}
return false;
}
inCheck(side) {
const ksq = this.findKingSq(side);
return ksq >= 0 && this.isAttacked(ksq, -side);
}
generateMoves(capturesOnly = false) {
const moves = [];
const s = this.side;
const opp = -s;
for (let sq = 0; sq < 64; sq++) {
const p = this.board[sq];
if (p === EMPTY || Math.sign(p) !== s)
continue;
const abs = Math.abs(p);
const file = sqFile(sq);
const rank = sqRank(sq);
if (abs === 1) {
const dir = s;
const promoRank = s === 1 ? 7 : 0;
const startRank = s === 1 ? 1 : 6;
const fwd = sq + dir * 8;
if (fwd >= 0 && fwd <= 63 && this.board[fwd] === EMPTY) {
if (sqRank(fwd) === promoRank) {
for (const pr of [WQ, WR, WB, WN])
moves.push(this.createMove(sq, fwd, FLAG_PROMO, s * pr));
} else if (!capturesOnly) {
moves.push(this.createMove(sq, fwd));
const fwd2 = fwd + dir * 8;
if (rank === startRank && fwd2 >= 0 && fwd2 <= 63 && this.board[fwd2] === EMPTY) {
moves.push(this.createMove(sq, fwd2));
}
}
}
for (const df of [-1, 1]) {
const cf = file + df;
if (cf < 0 || cf > 7)
continue;
const csq = fwd + df;
if (csq < 0 || csq > 63)
continue;
if (this.board[csq] !== EMPTY && Math.sign(this.board[csq]) === opp) {
if (sqRank(csq) === promoRank) {
for (const pr of [WQ, WR, WB, WN])
moves.push(this.createMove(sq, csq, FLAG_PROMO, s * pr));
} else {
moves.push(this.createMove(sq, csq));
}
} else if (csq === this.epSquare) {
moves.push(this.createMove(sq, csq, FLAG_EP));
}
}
} else if (abs === 2) {
for (const off of [-17, -15, -10, -6, 6, 10, 15, 17]) {
const t = sq + off;
if (t < 0 || t > 63 || Math.abs(sqFile(t) - file) > 2)
continue;
const tp = this.board[t];
if (tp !== EMPTY && Math.sign(tp) === s)
continue;
if (capturesOnly && tp === EMPTY)
continue;
moves.push(this.createMove(sq, t));
}
} else if (abs === 6) {
for (let dr = -1; dr <= 1; dr++) {
for (let df = -1; df <= 1; df++) {
if (!dr && !df)
continue;
const t = sq + dr * 8 + df;
if (t < 0 || t > 63 || Math.abs(sqFile(t) - file) > 1)
continue;
const tp = this.board[t];
if (tp !== EMPTY && Math.sign(tp) === s)
continue;
if (capturesOnly && tp === EMPTY)
continue;
moves.push(this.createMove(sq, t));
}
}
if (!capturesOnly && !this.inCheck(s)) {
if (s === 1) {
if (this.castling & 1 && sq === 4 && this.board[5] === EMPTY && this.board[6] === EMPTY && !this.isAttacked(5, -1) && !this.isAttacked(6, -1)) {
moves.push(this.createMove(4, 6, FLAG_CASTLE));
}
if (this.castling & 2 && sq === 4 && this.board[3] === EMPTY && this.board[2] === EMPTY && this.board[1] === EMPTY && !this.isAttacked(3, -1) && !this.isAttacked(2, -1)) {
moves.push(this.createMove(4, 2, FLAG_CASTLE));
}
} else {
if (this.castling & 4 && sq === 60 && this.board[61] === EMPTY && this.board[62] === EMPTY && !this.isAttacked(61, 1) && !this.isAttacked(62, 1)) {
moves.push(this.createMove(60, 62, FLAG_CASTLE));
}
if (this.castling & 8 && sq === 60 && this.board[59] === EMPTY && this.board[58] === EMPTY && this.board[57] === EMPTY && !this.isAttacked(59, 1) && !this.isAttacked(58, 1)) {
moves.push(this.createMove(60, 58, FLAG_CASTLE));
}
}
}
} else {
const dirs = abs === 3 ? [9, 7, -9, -7] : abs === 4 ? [8, -8, 1, -1] : [9, 7, -9, -7, 8, -8, 1, -1];
for (const dir of dirs) {
let t = sq + dir;
while (t >= 0 && t <= 63) {
const fdiff = Math.abs(sqFile(t) - sqFile(t - dir));
if ((dir === 1 || dir === -1) && fdiff !== 1)
break;
if ((Math.abs(dir) === 7 || Math.abs(dir) === 9) && fdiff !== 1)
break;
const tp = this.board[t];
if (tp !== EMPTY && Math.sign(tp) === s)
break;
if (!capturesOnly || tp !== EMPTY)
moves.push(this.createMove(sq, t));
if (tp !== EMPTY)
break;
t += dir;
}
}
}
}
return moves;
}
generateLegalMoves(capturesOnly = false) {
const pseudo = this.generateMoves(capturesOnly);
const legal = [];
for (const mv of pseudo) {
this.makeMove(mv);
if (!this.inCheck(-this.side))
legal.push(mv);
this.unmakeMove(mv);
}
return legal;
}
moveToUci(mv) {
let s = sqName(mv.from) + sqName(mv.to);
if (mv.flags & FLAG_PROMO) {
s += "nbrq"[Math.abs(mv.promo) - 2];
}
return s;
}
// ---- Evaluation ----
evaluate() {
let mgScore = 0, egScore = 0, phase = 0;
let wBishops = 0, bBishops = 0;
const phaseVal = { 2: 1, 3: 1, 4: 2, 5: 4 };
for (let sq = 0; sq < 64; sq++) {
const p2 = this.board[sq];
if (p2 === EMPTY)
continue;
const abs = Math.abs(p2);
const side = Math.sign(p2);
const val = PIECE_VAL[abs];
const pstSq = side === 1 ? sq : mirrorSq(sq);
let pstVal = 0;
if (abs <= 5 && PST[abs])
pstVal = PST[abs][pstSq];
let mgKing = 0, egKing = 0;
if (abs === 6) {
mgKing = PST_KING_MG[pstSq];
egKing = PST_KING_EG[pstSq];
}
if (abs === 3) {
if (side === 1)
wBishops++;
else
bBishops++;
}
if (abs >= 2 && abs <= 5)
phase += phaseVal[abs] || 0;
const material = val * side;
mgScore += material + (abs === 6 ? mgKing * side : pstVal * side);
egScore += material + (abs === 6 ? egKing * side : pstVal * side);
}
if (wBishops >= 2) {
mgScore += 30;
egScore += 50;
}
if (bBishops >= 2) {
mgScore -= 30;
egScore -= 50;
}
for (let f = 0; f < 8; f++) {
let wPawnsOnFile = 0, bPawnsOnFile = 0;
for (let r = 0; r < 8; r++) {
const p2 = this.board[r * 8 + f];
if (p2 === WP)
wPawnsOnFile++;
if (p2 === BP)
bPawnsOnFile++;
}
if (wPawnsOnFile > 1) {
mgScore -= 10 * (wPawnsOnFile - 1);
egScore -= 20 * (wPawnsOnFile - 1);
}
if (bPawnsOnFile > 1) {
mgScore += 10 * (bPawnsOnFile - 1);
egScore += 20 * (bPawnsOnFile - 1);
}
}
for (let sq = 0; sq < 64; sq++) {
const p2 = this.board[sq];
if (Math.abs(p2) !== 4)
continue;
const f = sqFile(sq);
let hasFriendlyPawn = false, hasEnemyPawn = false;
for (let r = 0; r < 8; r++) {
const pp = this.board[r * 8 + f];
if (pp === Math.sign(p2) * WP)
hasFriendlyPawn = true;
if (pp === -Math.sign(p2) * WP)
hasEnemyPawn = true;
}
if (!hasFriendlyPawn && !hasEnemyPawn) {
mgScore += 20 * Math.sign(p2);
egScore += 20 * Math.sign(p2);
} else if (!hasFriendlyPawn) {
mgScore += 10 * Math.sign(p2);
egScore += 10 * Math.sign(p2);
}
}
for (const side of [1, -1]) {
const ksq = this.findKingSq(side);
if (ksq < 0)
continue;
const kf = sqFile(ksq);
const kr = sqRank(ksq);
const shieldRank = kr + side;
if (shieldRank >= 0 && shieldRank <= 7) {
let shield = 0;
for (let df = -1; df <= 1; df++) {
const sf = kf + df;
if (sf < 0 || sf > 7)
continue;
if (this.board[shieldRank * 8 + sf] === side * WP)
shield++;
}
mgScore += shield * 15 * side;
}
}
const maxPhase = 24;
const p = Math.min(phase, maxPhase);
const score = Math.round((mgScore * p + egScore * (maxPhase - p)) / maxPhase);
return score * this.side;
}
// ---- Search ----
scoreMoves(moves, ply, ttMove) {
const scores = new Int32Array(moves.length);
for (let i = 0; i < moves.length; i++) {
const mv = moves[i];
let s = 0;
if (ttMove && mv.from === ttMove.from && mv.to === ttMove.to) {
s = 1e5;
} else if (mv.captured !== EMPTY) {
s = 1e4 + PIECE_VAL[Math.abs(mv.captured)] * 10 - PIECE_VAL[Math.abs(mv.piece)];
}
if (mv.flags & FLAG_PROMO)
s += 8e3 + PIECE_VAL[Math.abs(mv.promo)];
if (this.killers[ply] && this.killers[ply].includes(mv.from * 64 + mv.to))
s += 5e3;
s += this.history[mv.from * 64 + mv.to];
scores[i] = s;
}
return scores;
}
// Lazy selection sort: pick best move for position i, swap it in place
pickMove(moves, scores, startIdx) {
let bestIdx = startIdx;
let bestScore = scores[startIdx];
for (let j = startIdx + 1; j < moves.length; j++) {
if (scores[j] > bestScore) {
bestScore = scores[j];
bestIdx = j;
}
}
if (bestIdx !== startIdx) {
const tmpMv = moves[startIdx];
moves[startIdx] = moves[bestIdx];
moves[bestIdx] = tmpMv;
const tmpSc = scores[startIdx];
scores[startIdx] = scores[bestIdx];
scores[bestIdx] = tmpSc;
}
}
// --- Transposition Table ---
ttKey() {
let key = "";
for (let i = 0; i < 64; i++)
key += this.board[i] + ",";
key += this.side + "," + this.castling + "," + this.epSquare;
return key;
}
ttProbe(depth, alpha, beta) {
const entry = this.tt.get(this.ttKey());
if (!entry || entry.depth < depth)
return null;
if (entry.flag === TT_EXACT)
return { score: entry.score, move: entry.move };
if (entry.flag === TT_ALPHA && entry.score <= alpha)
return { score: alpha, move: entry.move };
if (entry.flag === TT_BETA && entry.score >= beta)
return { score: beta, move: entry.move };
return { score: null, move: entry.move };
}
ttStore(depth, score, flag, move) {
const key = this.ttKey();
const existing = this.tt.get(key);
if (!existing || existing.depth <= depth) {
this.tt.set(key, { depth, score, flag, move });
if (this.tt.size > TT_SIZE) {
const firstKey = this.tt.keys().next().value;
this.tt.delete(firstKey);
}
}
}
quiesce(alpha, beta, ply) {
this.nodes++;
if (this.nodes % 4096 === 0 && performance.now() - this.startTime > this.timeLimit) {
this.stopped = true;
return 0;
}
const inChk = this.inCheck(this.side);
if (!inChk) {
const standPat = this.evaluate();
if (standPat >= beta)
return beta;
if (standPat > alpha)
alpha = standPat;
}
const moves = this.generateLegalMoves(!inChk);
if (inChk && moves.length === 0)
return -(MATE_SCORE - ply);
const standPatForDelta = inChk ? -MATE_SCORE : alpha;
const scores = this.scoreMoves(moves, ply, null);
for (let i = 0; i < moves.length; i++) {
this.pickMove(moves, scores, i);
const mv = moves[i];
if (!inChk && mv.captured !== EMPTY) {
const delta = PIECE_VAL[Math.abs(mv.captured)] + 200;
if (standPatForDelta + delta < alpha)
continue;
}
this.makeMove(mv);
const score = -this.quiesce(-beta, -alpha, ply + 1);
this.unmakeMove(mv);
if (this.stopped)
return 0;
if (score >= beta)
return beta;
if (score > alpha)
alpha = score;
}
return alpha;
}
negamax(depth, alpha, beta, ply, pvLine) {
this.nodes++;
if (this.stopped)
return 0;
if (this.nodes % 4096 === 0 && performance.now() - this.startTime > this.timeLimit) {
this.stopped = true;
return 0;
}
if (depth <= 0)
return this.quiesce(alpha, beta, ply);
const inChk = this.inCheck(this.side);
if (inChk && ply < 20 && this.extensions < 6) {
depth++;
this.extensions++;
}
if (this.halfmove >= 100)
return 0;
let ttMove = null;
const ttEntry = this.ttProbe(depth, alpha, beta);
if (ttEntry) {
ttMove = ttEntry.move;
if (ttEntry.score !== null)
return ttEntry.score;
}
const moves = this.generateLegalMoves();
if (moves.length === 0) {
return inChk ? -(MATE_SCORE - ply) : 0;
}
if (!inChk && depth >= 3 && ply > 0) {
this.stateStack.push({
castling: this.castling,
epSquare: this.epSquare,
halfmove: this.halfmove,
fullmove: this.fullmove,
wKingSq: this.wKingSq,
bKingSq: this.bKingSq
});
this.epSquare = -1;
this.side = -this.side;
const nullScore = -this.negamax(depth - 3, -beta, -beta + 1, ply + 1, []);
this.side = -this.side;
const st = this.stateStack.pop();
this.castling = st.castling;
this.epSquare = st.epSquare;
this.halfmove = st.halfmove;
this.fullmove = st.fullmove;
this.wKingSq = st.wKingSq;
this.bKingSq = st.bKingSq;
if (this.stopped)
return 0;
if (nullScore >= beta)
return beta;
}
const scores = this.scoreMoves(moves, ply, ttMove);
const childPv = [];
let bestMoveInNode = null;
let origAlpha = alpha;
for (let i = 0; i < moves.length; i++) {
this.pickMove(moves, scores, i);
const mv = moves[i];
this.makeMove(mv);
childPv.length = 0;
const score = -this.negamax(depth - 1, -beta, -alpha, ply + 1, childPv);
this.unmakeMove(mv);
if (this.stopped)
return 0;
if (score >= beta) {
if (mv.captured === EMPTY) {
if (!this.killers[ply])
this.killers[ply] = [];
const key = mv.from * 64 + mv.to;
if (!this.killers[ply].includes(key)) {
this.killers[ply].unshift(key);
if (this.killers[ply].length > 2)
this.killers[ply].pop();
}
this.history[mv.from * 64 + mv.to] += depth * depth;
}
this.ttStore(depth, beta, TT_BETA, mv);
return beta;
}
if (score > alpha) {
alpha = score;
bestMoveInNode = mv;
pvLine.length = 0;
pvLine.push(mv);
pvLine.push(...childPv);
}
}
const flag = alpha > origAlpha ? TT_EXACT : TT_ALPHA;
this.ttStore(depth, alpha, flag, bestMoveInNode || moves[0]);
return alpha;
}
searchRoot(maxDepth, timeLimitMs) {
this.nodes = 0;
this.startTime = performance.now();
this.timeLimit = timeLimitMs;
this.stopped = false;
this.killers = [];
this.history.fill(0);
this.tt.clear();
let bestMove = null;
let bestScore = 0;
let bestPv = [];
let completedDepth = 0;
for (let d = 1; d <= maxDepth; d++) {
this.extensions = 0;
if (d > 1) {
for (let i = 0; i < this.history.length; i++) {
this.history[i] >>= 1;
}
}
const pvLine = [];
const score = this.negamax(d, -MATE_SCORE - 1, MATE_SCORE + 1, 0, pvLine);
if (this.stopped && d > 1)
break;
if (pvLine.length > 0) {
bestMove = pvLine[0];
bestScore = score;
bestPv = pvLine.slice();
completedDepth = d;
}
if (Math.abs(score) > MATE_SCORE - 100)
break;
}
const whiteScore = bestScore * this.side;
return { move: bestMove, score: whiteScore, pv: bestPv, depth: completedDepth, nodes: this.nodes };
}
analyze(fen, depth) {
this.loadFen(fen);
const timeMs = Math.min(depth * 150, 500);
const searchDepth = Math.min(depth, 6);
const result = this.searchRoot(searchDepth, timeMs);
if (!result.move) {
return { success: false, bestmove: "(none)", evaluation: 0 };
}
const uci = this.moveToUci(result.move);
const pvStr = result.pv.map((m) => this.moveToUci(m)).join(" ");
let scoreObj;
if (Math.abs(result.score) > MATE_SCORE - 200) {
const mateIn = Math.ceil((MATE_SCORE - Math.abs(result.score)) / 2);
scoreObj = { mate: result.score > 0 ? mateIn : -mateIn };
} else {
scoreObj = { cp: result.score };
}
return {
success: true,
bestmove: uci,
evaluation: result.score / 100,
analysis: [{ uci, pv: pvStr, score: scoreObj }],
depth: result.depth,
nodes: result.nodes,
source: "local"
};
}
};
var localEngine = new LocalEngine();
function analyzeLocally(fen, depth) {
console.log(`GabiBot: \u{1F9E0} Local engine analyzing FEN: ${fen.substring(0, 20)}... | Depth: ${depth}`);
const start = performance.now();
const result = localEngine.analyze(fen, depth);
const elapsed = performance.now() - start;
console.log(`GabiBot: \u{1F9E0} Local engine done in ${elapsed.toFixed(0)}ms | ${result.nodes} nodes | Depth: ${result.depth} | Best: ${result.bestmove}`);
return result;
}
async function fetchEngineData(fen, depth, signal) {
const startTime = performance.now();
console.log(`GabiBot: \u{1F4E1} API request for FEN: ${fen.substring(0, 20)}... | Depth: ${depth}`);
const call = async (params) => {
const url = `${API_URL}?fen=${encodeURIComponent(fen)}&depth=${depth}&${params}`;
return new Promise((resolve, reject) => {
const abortHandler = () => reject(new DOMException("Aborted", "AbortError"));
if (signal?.aborted)
return reject(new DOMException("Aborted", "AbortError"));
signal?.addEventListener("abort", abortHandler, { once: true });
const timeoutId = setTimeout(() => {
signal?.removeEventListener("abort", abortHandler);
reject(new Error("timeout"));
}, ANALYZE_TIMEOUT_MS);
if (typeof GM_xmlhttpRequest !== "undefined") {
const req = GM_xmlhttpRequest({
method: "GET",
url,
headers: { Accept: "application/json" },
onload: (r) => {
clearTimeout(timeoutId);
signal?.removeEventListener("abort", abortHandler);
if (r.status >= 200 && r.status < 300) {
try {
const data = JSON.parse(r.responseText);
if (data.success === false)
reject(new Error("API success=false"));
else {
console.log(`GabiBot: \u2705 API ok in ${(performance.now() - startTime).toFixed(0)}ms`);
resolve(data);
}
} catch {
reject(new Error("Invalid JSON"));
}
} else
reject(new Error(`API error ${r.status}`));
},
onerror: () => {
clearTimeout(timeoutId);
signal?.removeEventListener("abort", abortHandler);
reject(new Error("Network error"));
},
ontimeout: () => {
clearTimeout(timeoutId);
signal?.removeEventListener("abort", abortHandler);
reject(new Error("timeout"));
}
});
signal?.addEventListener("abort", () => req.abort(), { once: true });
} else {
fetch(url, { method: "GET", headers: { Accept: "application/json" }, signal }).then(async (res) => {
clearTimeout(timeoutId);
if (!res.ok)
throw new Error(`API error ${res.status}`);
const data = await res.json();
if (data.success === false)
throw new Error("API success=false");
console.log(`GabiBot: \u2705 API ok in ${(performance.now() - startTime).toFixed(0)}ms`);
resolve(data);
}).catch((err) => {
clearTimeout(timeoutId);
signal?.removeEventListener("abort", abortHandler);
reject(err);
});
}
});
};
try {
return await call(`multipv=${MULTIPV}&mode=bestmove`);
} catch (e) {
if (e.name === "AbortError")
throw e;
throw e;
}
}
async function fetchAnalysis(fen, depth, signal) {
const cached = PositionCache.get(fen);
if (cached) {
console.log("GabiBot: \u{1F5C3}\uFE0F Using cached analysis");
return cached;
}
if (signal?.aborted || !BotState.hackEnabled)
throw new DOMException("Aborted", "AbortError");
const apiPromise = fetchEngineData(fen, depth, signal).then((data) => ({ ok: true, data })).catch((err) => ({ ok: false, error: err }));
BotState.statusInfo = "\u{1F9E0} Analyzing...";
const localResult = analyzeLocally(fen, depth);
const apiSettled = await Promise.race([
apiPromise,
new Promise((r) => setTimeout(r, 10)).then(() => null)
]);
if (apiSettled?.ok) {
console.log("GabiBot: \u2705 API beat local engine");
PositionCache.set(fen, apiSettled.data);
return apiSettled.data;
}
if (localResult.success) {
PositionCache.set(fen, localResult);
apiPromise.then((r) => {
if (r.ok) {
console.log("GabiBot: \u{1F4E1} API result arrived, cache upgraded");
PositionCache.set(fen, r.data);
}
});
return localResult;
}
const apiResult = await apiPromise;
if (apiResult.ok) {
PositionCache.set(fen, apiResult.data);
return apiResult.data;
}
if (apiResult.error?.name === "AbortError")
throw apiResult.error;
throw new Error("Both API and local engine failed");
}
function parseBestLine(data) {
const lines = [];
const pushLine = (uci, pv, score) => {
if (!uci || uci.length < 4)
return;
lines.push({ uci: uci.trim(), pv: (pv || "").trim(), score: score || {} });
};
const addFromArray = (arr) => arr.forEach((item) => {
const pv = item.pv || item.line || item.moves || "";
const uci = item.uci || (pv ? pv.split(" ")[0] : "");
const score = scoreFrom(item.score || item.evaluation || item.eval);
pushLine(uci, pv, score);
});
if (Array.isArray(data.analysis))
addFromArray(data.analysis);
else if (Array.isArray(data.lines))
addFromArray(data.lines);
else if (Array.isArray(data.pvs))
addFromArray(data.pvs);
if (!lines.length && typeof data.bestmove === "string") {
const parts = data.bestmove.split(" ");
let uci = parts.length > 1 ? parts[1] : parts[0];
if (uci === "bestmove" && parts[1])
uci = parts[1];
const pv = data.pv || data.continuation || uci;
const score = scoreFrom(data.evaluation);
pushLine(uci, pv, score);
}
lines.sort((a, b) => scoreNumeric(b.score) - scoreNumeric(a.score));
return lines[0] || null;
}
function isEnPassantCapture(fen, from, to, ourColor) {
const parts = fen.split(" ");
const ep = parts[3];
const fromPiece = pieceFromFenChar(fenCharAtSquare(fen, from));
if (!fromPiece || fromPiece.color !== ourColor || fromPiece.type !== "p")
return false;
return ep && ep !== "-" && to === ep && from[0] !== to[0];
}
var HANGING_THRESHOLDS = { 6: 100, 5: 90, 4: 60, 3: 40, 2: 40, 1: 15 };
var premoveEngine = new LocalEngine();
function evaluatePremove(fen, opponentUci, ourUci, ourColor, evalDisplay) {
if (!ourUci || ourUci.length < 4) {
return { execute: false, chance: 0, reasons: [], blocked: "Invalid move" };
}
let chance = getEvalBasedPremoveChance(evalDisplay, ourColor);
const reasons = [];
const oppSide = ourColor === "w" ? -1 : 1;
const ourSide = -oppSide;
if (!opponentUci || opponentUci.length < 4) {
return { execute: false, chance: 0, reasons: [], blocked: "No predicted opponent move" };
}
try {
premoveEngine.loadFen(fen);
const oppFrom = nameToSq(opponentUci.substring(0, 2));
const oppTo = nameToSq(opponentUci.substring(2, 4));
const oppMoves = premoveEngine.generateLegalMoves();
const oppMove = oppMoves.find((m) => m.from === oppFrom && m.to === oppTo);
if (!oppMove)
return { execute: false, chance: 0, reasons: [], blocked: "Opponent move not legal" };
premoveEngine.makeMove(oppMove);
const ourLegalMoves = premoveEngine.generateLegalMoves();
const ourFrom = nameToSq(ourUci.substring(0, 2));
const ourTo = nameToSq(ourUci.substring(2, 4));
const ourMove = ourLegalMoves.find((m) => m.from === ourFrom && m.to === ourTo);
if (!ourMove) {
premoveEngine.unmakeMove(oppMove);
return { execute: false, chance: 0, reasons: [], blocked: "Our move illegal after opponent plays" };
}
const movingAbs = Math.abs(ourMove.piece);
const destPieceAbs = movingAbs;
const capturedAbs = ourMove.captured !== EMPTY ? Math.abs(ourMove.captured) : 0;
const capturedVal = capturedAbs > 0 ? PIECE_VAL[capturedAbs] || 0 : 0;
const movedVal = PIECE_VAL[destPieceAbs] || 0;
if (destPieceAbs !== 6) {
premoveEngine.makeMove(ourMove);
const isDestAttacked = premoveEngine.isAttacked(ourTo, premoveEngine.side);
premoveEngine.unmakeMove(ourMove);
if (isDestAttacked && destPieceAbs >= 2) {
premoveEngine.makeMove(ourMove);
const oppAttacksPost = [];
const ourDefendsPost = [];
const oppReplies = premoveEngine.generateLegalMoves();
for (const reply of oppReplies) {
if (reply.to === ourTo)
oppAttacksPost.push(reply);
}
premoveEngine.unmakeMove(ourMove);
if (oppAttacksPost.length > 0 && capturedVal < movedVal) {
const riskThreshold = HANGING_THRESHOLDS[destPieceAbs] || 50;
const pieceNames = { 5: "queen", 4: "rook", 3: "bishop", 2: "knight" };
const pieceName = pieceNames[destPieceAbs] || "piece";
const defenderCount = premoveEngine.isAttacked(ourTo, premoveEngine.side) ? 1 : 0;
const lowestAttackerVal = Math.min(...oppAttacksPost.map((r) => PIECE_VAL[Math.abs(r.piece)] || 100));
if (defenderCount === 0 || lowestAttackerVal < movedVal) {
if (destPieceAbs >= 5) {
premoveEngine.unmakeMove(oppMove);
return { execute: false, chance: 0, reasons: [], blocked: `Hangs ${pieceName}` };
}
chance = Math.max(5, chance - riskThreshold);
reasons.push(`${pieceName} at risk`);
}
}
}
}
premoveEngine.makeMove(ourMove);
const ourKingSq = premoveEngine.findKingSq(ourSide);
if (ourKingSq >= 0) {
const kRank = sqRank(ourKingSq);
const isBackRank = ourSide === 1 && kRank === 0 || ourSide === -1 && kRank === 7;
if (isBackRank) {
const shieldRank = kRank + ourSide;
if (shieldRank >= 0 && shieldRank <= 7) {
const kFile = sqFile(ourKingSq);
let escapable = false;
for (let df = -1; df <= 1; df++) {
const sf = kFile + df;
if (sf < 0 || sf > 7)
continue;
const shieldSq = shieldRank * 8 + sf;
if (premoveEngine.board[shieldSq] === EMPTY && !premoveEngine.isAttacked(shieldSq, premoveEngine.side)) {
escapable = true;
break;
}
}
if (!escapable) {
const backRankAttacked = premoveEngine.isAttacked(ourKingSq, premoveEngine.side);
if (backRankAttacked) {
premoveEngine.unmakeMove(ourMove);
premoveEngine.unmakeMove(oppMove);
return { execute: false, chance: 0, reasons: [], blocked: "Back-rank mate threat" };
}
chance = Math.max(10, chance - 20);
reasons.push("back-rank weak");
}
}
}
}
premoveEngine.unmakeMove(ourMove);
if (ourLegalMoves.length === 1) {
chance = Math.min(95, chance + 40);
reasons.push("forced");
} else if (ourLegalMoves.length <= 3) {
chance = Math.min(95, chance + 15);
reasons.push("few options");
}
if (ourTo === oppTo) {
chance = Math.min(95, chance + 20);
reasons.push("recapture");
}
premoveEngine.makeMove(ourMove);
if (premoveEngine.inCheck(premoveEngine.side)) {
chance = Math.min(95, chance + 10);
reasons.push("check");
}
premoveEngine.unmakeMove(ourMove);
const destAttacked = premoveEngine.isAttacked(ourTo, -premoveEngine.side);
if (!destAttacked) {
chance = Math.min(95, chance + 10);
reasons.push("safe sq");
}
const centerSquares = [nameToSq("d4"), nameToSq("d5"), nameToSq("e4"), nameToSq("e5")];
if (centerSquares.includes(ourTo)) {
chance = Math.min(95, chance + 5);
reasons.push("center");
}
if (ourLegalMoves.length > 1 && ourLegalMoves.length <= 30) {
premoveEngine.makeMove(ourMove);
const ourSearchResult = premoveEngine.searchRoot(3, 200);
const ourScore = ourSearchResult.score ? -ourSearchResult.score : -premoveEngine.evaluate();
premoveEngine.unmakeMove(ourMove);
let secondBest = -Infinity;
for (const alt of ourLegalMoves) {
if (alt.from === ourFrom && alt.to === ourTo)
continue;
premoveEngine.makeMove(alt);
const altScore = -premoveEngine.evaluate();
premoveEngine.unmakeMove(alt);
if (altScore > secondBest)
secondBest = altScore;
}
if (secondBest > -Infinity && ourScore - secondBest >= 150) {
chance = Math.min(95, chance + 25);
reasons.push("dominant");
}
}
premoveEngine.unmakeMove(oppMove);
const oppScoredMoves = [];
for (const oMove of oppMoves) {
premoveEngine.makeMove(oMove);
const score = -premoveEngine.evaluate();
premoveEngine.unmakeMove(oMove);
oppScoredMoves.push({ move: oMove, score });
}
oppScoredMoves.sort((a, b) => b.score - a.score);
const topOppMoves = oppScoredMoves.filter((m) => !(m.move.from === oppFrom && m.move.to === oppTo)).slice(0, 3);
let illegalCount = 0;
let badScoreCount = 0;
for (const { move: altOppMove } of topOppMoves) {
premoveEngine.makeMove(altOppMove);
const altLegal = premoveEngine.generateLegalMoves();
const altOurMove = altLegal.find((m) => m.from === ourFrom && m.to === ourTo);
if (!altOurMove) {
illegalCount++;
} else {
premoveEngine.makeMove(altOurMove);
const postScore = -premoveEngine.evaluate();
premoveEngine.unmakeMove(altOurMove);
let bestAlt = -Infinity;
for (const alt of altLegal) {
if (alt.from === ourFrom && alt.to === ourTo)
continue;
premoveEngine.makeMove(alt);
const altS = -premoveEngine.evaluate();
premoveEngine.unmakeMove(alt);
if (altS > bestAlt)
bestAlt = altS;
}
if (bestAlt > -Infinity && bestAlt - postScore >= 200) {
badScoreCount++;
}
}
premoveEngine.unmakeMove(altOppMove);
}
if (topOppMoves.length >= 2 && illegalCount >= 2) {
chance = Math.max(5, chance - 35);
reasons.push("unstable (illegal)");
} else if (illegalCount >= 1) {
chance = Math.max(10, chance - 15);
reasons.push("sometimes illegal");
}
if (topOppMoves.length >= 2 && badScoreCount >= 2) {
chance = Math.max(5, chance - 30);
reasons.push("unstable (bad)");
} else if (badScoreCount >= 1) {
chance = Math.max(10, chance - 10);
reasons.push("risky alt");
}
} catch (e) {
console.warn("GabiBot: evaluatePremove error:", e);
return { execute: false, chance: 0, reasons: [], blocked: "Evaluation error" };
}
chance = Math.min(95, Math.max(0, Math.round(chance)));
const execute = chance > 0;
return { execute, chance, reasons, blocked: null };
}
function shouldPremove(uci, fen) {
if (!uci || uci.length < 4)
return false;
const game = getGame();
const ourColor = getPlayerColor(game);
const from = uci.substring(0, 2);
const to = uci.substring(2, 4);
const fromPiece = pieceFromFenChar(fenCharAtSquare(fen, from));
const toPiece = pieceFromFenChar(fenCharAtSquare(fen, to));
if (!fromPiece || fromPiece.color !== ourColor)
return false;
if (BotState.premoveMode === "every")
return true;
if (BotState.premoveMode === "capture") {
return !!(toPiece && toPiece.color !== ourColor) || isEnPassantCapture(fen, from, to, ourColor);
}
if (BotState.premoveMode === "filter")
return !!BotState.premovePieces[fromPiece.type];
return false;
}
function getEvalBasedPremoveChance(evaluation, ourColor) {
if (!BotState.premoveEnabled)
return 0;
let evalScore = 0;
if (typeof evaluation === "string") {
if (evaluation === "-" || evaluation === "Error")
return 0;
if (evaluation.includes("M")) {
const mateNum = parseInt(evaluation.replace("M", "").replace("+", ""), 10);
if (!isNaN(mateNum))
return (ourColor === "w" ? mateNum : -mateNum) > 0 ? 100 : 25;
}
evalScore = parseFloat(evaluation);
} else
evalScore = parseFloat(evaluation);
if (isNaN(evalScore))
return 0;
const ourEval = ourColor === "w" ? evalScore : -evalScore;
if (ourEval >= 3)
return 90;
if (ourEval >= 2)
return 75;
if (ourEval >= 1)
return 55;
if (ourEval >= 0.5)
return 40;
if (ourEval >= 0)
return 30;
if (ourEval >= -0.5)
return 25;
return 20;
}
function getOurMoveFromPV(pv, ourColor, sideToMove) {
if (!pv)
return null;
const moves = pv.trim().split(/\s+/).filter(Boolean);
if (!moves.length)
return null;
return moves[sideToMove === ourColor ? 0 : 1] || null;
}
var scheduledMainFen = "";
var scheduledPremoveFen = "";
function scheduleAnalysis(kind, fen, tickCallback) {
if (kind === "main" && scheduledMainFen === fen)
return;
if (kind !== "main" && scheduledPremoveFen === fen)
return;
if (kind === "main")
scheduledMainFen = fen;
else
scheduledPremoveFen = fen;
const analysisId = ++currentAnalysisId;
if (currentAbortController) {
currentAbortController.abort("superseded");
currentAbortController = null;
}
const ctrl = new AbortController();
currentAbortController = ctrl;
const run = async () => {
analysisRunning = true;
if (analysisId !== currentAnalysisId || !BotState.hackEnabled) {
analysisRunning = false;
return;
}
invalidateGameCache();
const game = getGame();
if (!game) {
analysisRunning = false;
return;
}
if (kind === "main" && lastFenProcessedMain === fen) {
analysisRunning = false;
return;
}
if (kind !== "main" && lastFenProcessedPremove === fen) {
analysisRunning = false;
return;
}
try {
BotState.statusInfo = kind === "main" ? "\u{1F504} Analyzing..." : "\u{1F504} Analyzing (premove)...";
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
const randomDepth = getRandomDepth(BotState.botPower);
if (analysisId !== currentAnalysisId) {
ctrl.abort("superseded");
return;
}
const data = await fetchAnalysis(fen, randomDepth, ctrl.signal);
if (analysisId !== currentAnalysisId)
return;
const sourceLabel = data.source === "local" ? " [local]" : "";
const best = parseBestLine(data);
if (kind === "main") {
BotState.bestMove = best?.uci || "-";
BotState.currentEvaluation = scoreToDisplay(best?.score);
BotState.principalVariation = best?.pv || "Not available";
BotState.statusInfo = `\u2713 Ready${sourceLabel}`;
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
if (best) {
const from = best.uci.substring(0, 2);
const to = best.uci.substring(2, 4);
const promo = best.uci.length >= 5 ? best.uci[4] : null;
await executeMove(from, to, fen, promo, tickCallback);
}
lastFenProcessedMain = fen;
} else {
const ourColor = getPlayerColor(game);
const stm = getSideToMove(game);
const pvMoves = (best?.pv || "").trim().split(/\s+/).filter(Boolean);
const opponentUci = stm !== ourColor && pvMoves.length > 0 ? pvMoves[0] : null;
const ourUci = getOurMoveFromPV(best?.pv || "", ourColor, stm) || (stm === ourColor ? best?.uci || null : null);
const premoveEvalDisplay = scoreToDisplay(best?.score);
if (!ourUci) {
BotState.statusInfo = `Premove unavailable (no PV)${sourceLabel}`;
BotState.currentPremoveReasons = "";
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
lastFenProcessedPremove = fen;
return;
}
if (!shouldPremove(ourUci, fen)) {
BotState.statusInfo = `Premove skipped (${BotState.premoveMode})${sourceLabel}`;
BotState.currentPremoveReasons = "";
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
lastFenProcessedPremove = fen;
return;
}
const premoveResult = evaluatePremove(fen, opponentUci, ourUci, ourColor, premoveEvalDisplay);
BotState.currentPremoveReasons = premoveResult.reasons.length > 0 ? premoveResult.reasons.join(", ") : "";
if (premoveResult.blocked) {
BotState.statusInfo = `\u{1F6E1}\uFE0F Premove blocked: ${premoveResult.blocked}${sourceLabel}`;
BotState.currentPremoveReasons = "";
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
lastFenProcessedPremove = fen;
return;
}
if (!premoveResult.execute) {
BotState.statusInfo = `Premove skipped (no confidence)${sourceLabel}`;
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
lastFenProcessedPremove = fen;
return;
}
const currentChance = premoveResult.chance;
BotState.currentPremoveChance = currentChance;
if (premoveResult.reasons.length > 0) {
console.log(`GabiBot: \u{1F9E0} Premove [${premoveResult.reasons.join(", ")}] \u2192 ${currentChance}%`);
}
const roll = Math.random() * 100;
if (roll > currentChance) {
const reasonTag = premoveResult.reasons.length > 0 ? ` [${premoveResult.reasons.join(", ")}]` : "";
BotState.statusInfo = `Premove skipped: eval ${premoveEvalDisplay}${reasonTag}, ${roll.toFixed(0)}% > ${currentChance}%${sourceLabel}`;
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
lastFenProcessedPremove = fen;
return;
}
const from = ourUci.substring(0, 2);
const to = ourUci.substring(2, 4);
clearArrows();
drawArrow(from, to, "rgba(80, 180, 255, 0.7)", 3);
await simulateClickMove(from, to);
await sleep(80);
lastPremoveFen = fen;
lastPremoveUci = ourUci;
const reasonSuffix = premoveResult.reasons.length > 0 ? ` [${premoveResult.reasons.join(", ")}]` : "";
BotState.statusInfo = `\u2705 Premove: ${ourUci} (${currentChance}%)${reasonSuffix}${sourceLabel}`;
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
lastFenProcessedPremove = fen;
}
} catch (error) {
if (String(error?.name || error).toLowerCase().includes("abort") || String(error?.message || error).toLowerCase().includes("superseded")) {
} else {
console.error("GabiBot Error:", error);
BotState.statusInfo = "\u274C Analysis Error";
BotState.currentEvaluation = "Error";
if (BotState.onUpdateDisplay)
BotState.onUpdateDisplay(pa());
}
} finally {
if (kind === "main" && scheduledMainFen === fen)
scheduledMainFen = "";
else if (kind !== "main" && scheduledPremoveFen === fen)
scheduledPremoveFen = "";
if (currentAbortController === ctrl)
currentAbortController = null;
analysisRunning = false;
}
};
run();
}
// src/index.js
(async function() {
"use strict";
if (window.__GABIBOT_RUNNING__) {
console.log("GabiBot: Already running, skipping init.");
return;
}
window.__GABIBOT_RUNNING__ = true;
console.log("GabiBot: Script loaded, waiting for board...");
let tickTimer = null;
let gameStartInterval = null;
let gameEndInterval = null;
let lastFenSeen = "";
let boardMoveObserver = null;
async function init() {
try {
BotState.onUpdateDisplay = (playingAs) => ui.updateDisplay(playingAs);
const board = await waitForElement(".board, chess-board, .board-layout-vertical, .board-layout-horizontal").catch(() => null);
await buildUI();
attachToBoard(board || qs("chess-board") || qs(".board") || qs('[class*="board"]'));
startDomBoardWatcher();
startAutoWatchers();
startStateWatcher();
console.log("GabiBot: Initialized.");
} catch (error) {
console.error("GabiBot Error:", error);
alert("GabiBot: Could not find chess board. Please refresh or check console.");
}
}
function tick() {
if (!BotState.hackEnabled)
return;
invalidateGameCache();
const game = getGame();
if (!game)
return;
if (game.isGameOver && game.isGameOver()) {
BotState.currentEvaluation = "GAME OVER";
BotState.bestMove = "-";
BotState.principalVariation = "Game ended";
BotState.statusInfo = "Game finished";
clearArrows();
ui.updateDisplay(pa());
return;
}
const fen = getFen(game);
if (!fen)
return;
if (fen !== lastFenSeen) {
lastFenSeen = fen;
cancelPendingMove();
clearArrows();
setLastPremoveFen("");
setLastPremoveUci("");
}
if (isPlayersTurn(game)) {
if (getLastFenProcessedMain() !== fen) {
scheduleAnalysis("main", fen, () => tick());
}
} else {
if (BotState.premoveEnabled) {
if (getLastFenProcessedPremove() !== fen) {
scheduleAnalysis("premove", fen);
} else {
const chanceEl = qs('[name="premoveChance"] .itemState');
if (chanceEl && BotState.currentPremoveChance !== void 0) {
chanceEl.textContent = `${Math.round(BotState.currentPremoveChance)}%`;
}
BotState.statusInfo = getLastPremoveUci() && getLastPremoveFen() === fen ? "Waiting (premove ready)..." : "Waiting for opponent...";
ui.updateDisplay(pa());
}
} else {
const chanceEl = qs('[name="premoveChance"] .itemState');
if (chanceEl)
chanceEl.textContent = "0%";
BotState.statusInfo = "Waiting for opponent...";
ui.updateDisplay(pa());
}
}
}
function startBoardMoveObserver() {
stopBoardMoveObserver();
const board = getGame() && (document.querySelector("chess-board") || document.querySelector(".board"));
if (!board)
return;
let debounceTimer = null;
boardMoveObserver = new MutationObserver(() => {
if (!BotState.hackEnabled)
return;
if (debounceTimer)
return;
debounceTimer = setTimeout(() => {
debounceTimer = null;
invalidateGameCache();
tick();
}, 50);
});
boardMoveObserver.observe(board, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["class", "style", "data-piece"]
});
}
function stopBoardMoveObserver() {
if (boardMoveObserver) {
boardMoveObserver.disconnect();
boardMoveObserver = null;
}
}
function startTickLoop() {
stopTickLoop();
startBoardMoveObserver();
const interval = Math.max(100, 1100 - (Number(BotState.updateSpeed) || 8) * 100);
const scheduleNext = () => {
tickTimer = setTimeout(() => {
tick();
if (BotState.hackEnabled)
scheduleNext();
}, interval);
};
tick();
scheduleNext();
}
function stopTickLoop() {
if (tickTimer)
clearTimeout(tickTimer);
tickTimer = null;
stopBoardMoveObserver();
}
function startStateWatcher() {
let lastHackEnabled = BotState.hackEnabled;
let lastUpdateSpeed = BotState.updateSpeed;
let lastPremoveEnabled = BotState.premoveEnabled;
setInterval(() => {
if (BotState.hackEnabled !== lastHackEnabled) {
lastHackEnabled = BotState.hackEnabled;
if (BotState.hackEnabled) {
BotState.statusInfo = "Ready";
ui.updateDisplay(pa());
startTickLoop();
} else {
stopTickLoop();
PositionCache.clear();
clearArrows();
cancelPendingMove();
BotState.statusInfo = "Bot disabled";
BotState.currentEvaluation = "-";
BotState.bestMove = "-";
ui.updateDisplay(pa());
}
ui.Settings.save();
}
if (BotState.updateSpeed !== lastUpdateSpeed) {
lastUpdateSpeed = BotState.updateSpeed;
if (BotState.hackEnabled)
startTickLoop();
}
if (BotState.premoveEnabled !== lastPremoveEnabled) {
lastPremoveEnabled = BotState.premoveEnabled;
if (BotState.hackEnabled)
startTickLoop();
}
}, 200);
if (BotState.hackEnabled)
startTickLoop();
}
function startAutoWatchers() {
if (gameStartInterval)
clearInterval(gameStartInterval);
if (gameEndInterval)
clearInterval(gameEndInterval);
let gameEndDetected = false;
gameEndInterval = setInterval(() => {
const gameOverModal = qs(".game-over-modal-content");
if (gameOverModal && !gameEndDetected) {
console.log("GabiBot: Game over detected");
clearArrows();
cancelPendingMove();
BotState.statusInfo = "Game ended, preparing new game...";
BotState.currentEvaluation = "-";
BotState.bestMove = "-";
ui?.updateDisplay(pa());
gameEndDetected = true;
if (BotState.autoRematch) {
console.log("GabiBot: Auto-rematch enabled");
setTimeout(() => {
const modal = qs(".game-over-modal-content");
if (!modal)
return console.log("GabiBot: [2s] Modal closed");
const btn = qsa("button", modal).find(
(b) => /rematch/i.test((b.textContent || "").trim()) || /rematch/i.test((b.getAttribute?.("aria-label") || "").trim())
);
if (btn)
btn.click();
}, 2e3);
setTimeout(() => {
const modal = qs(".game-over-modal-content");
if (!modal)
return;
const btn = qsa("button", modal).find((b) => /new.*\d+.*min/i.test(b.textContent || ""));
if (btn)
btn.click();
}, 12e3);
setTimeout(async () => {
const modal = qs(".game-over-modal-content");
if (!modal)
return;
const closeBtn = qs('[aria-label="Close"]', modal);
if (closeBtn) {
closeBtn.click();
await sleep(500);
}
const tab = qs('[data-tab="newGame"]') || qsa(".tabs-tab").find((t) => /new.*game/i.test(t.textContent || ""));
if (tab) {
tab.click();
await sleep(400);
const startBtn = qsa("button").find((b) => /start.*game/i.test((b.textContent || "").trim()));
if (startBtn)
startBtn.click();
}
}, 22e3);
}
}
if (!gameOverModal && gameEndDetected) {
console.log("GabiBot: New game started, bot analyzing...");
gameEndDetected = false;
setLastFenProcessedMain("");
setLastFenProcessedPremove("");
setLastPremoveFen("");
setLastPremoveUci("");
lastFenSeen = "";
if (BotState.hackEnabled) {
BotState.statusInfo = "Ready";
ui?.updateDisplay(pa());
setTimeout(() => {
if (BotState.hackEnabled)
tick();
}, 500);
}
}
}, 1e3);
}
setTimeout(init, 3e3);
})();
})();