在您安裝前,Greasy Fork希望您了解本腳本包含“負面功能”,可能幫助腳本的作者獲利,而不能給你帶來任何收益。
此腳本只有在您 註冊後才能使用全部的功能, 例如加入群組, 訂閱頻道, 或是點讚頁面。
Super chess Bot is a tournament level bullet bot
// ==UserScript== // @name ♟Super-chess-Bot // @namespace http://tampermonkey.net/ // @version 8.1 // @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 none // @antifeature membership // ==/UserScript== (async function () { 'use strict'; // Single-instance guard to prevent duplicate UI/intervals on SPA navigations if (window.__GABIBOT_RUNNING__) { console.log('GabiBot: Already running, skipping init.'); return; } window.__GABIBOT_RUNNING__ = true; // Engine + logic constants const API_URL = 'https://stockfish.online/api/s/v2.php'; const MULTIPV = 1; const ANALYZE_TIMEOUT_MS = 3000; // ⚡ 8000 → 3000ms for bullet const AUTO_MOVE_BASE = 800; // ⚡ 5000 → 800ms for bullet const AUTO_MOVE_STEP = 300; // ⚡ 500 → 300ms for bullet const RANDOM_JITTER_MIN = 50; // ⚡ 120 → 50ms for bullet console.log('GabiBot: Script loaded, waiting for board...'); // Debounce helper function debounce(fn, wait = 150) { let t = null; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); }; } // Position cache system const PositionCache = {}; function getRandomDepth() { const minDepth = 5; const maxDepth = Math.max(BotState.botPower || 10, minDepth); return Math.floor(Math.random() * (maxDepth - minDepth + 1)) + minDepth; } function getHumanDelay(baseDelay, randomDelay) { return baseDelay + Math.floor(Math.random() * randomDelay); } // Helpers const sleep = (ms) => new Promise(r => setTimeout(r, ms)); const qs = (sel, root = document) => root.querySelector(sel); const qsa = (sel, root = document) => Array.from(root.querySelectorAll(sel)); async function waitForElement(selector, timeout = 15000) { 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); }); } // Encapsulated state (avoid global pollution) const BotState = { hackEnabled: 0, botPower: 8, updateSpeed: 10, autoMove: 1, autoMoveSpeed: 8, randomDelay: 300, currentEvaluation: '-', bestMove: '-', principalVariation: '-', statusInfo: 'Ready', premoveEnabled: 0, premoveMode: 'every', premovePieces: { q: 1, r: 1, b: 1, n: 1, k: 1, p: 1 }, premoveChance: 85, autoRematch: 0 }; // Global state let ui = null; let boardCtx = null; // { boardEl, drawingBoard, ctx, evalBarWrap, resizeObserver, cancelPendingOnUserAction, touchOpts, detachListeners } let domObserver = null; let tickTimer = null; let gameStartInterval = null; let gameEndInterval = null; // Analysis queue let analysisQueue = Promise.resolve(); let currentAnalysisId = 0; // Tick state let lastFenProcessedMain = ''; let lastFenProcessedPremove = ''; let lastFenSeen = ''; let pendingMoveTimeoutId = null; // Premove state let lastPremoveFen = ''; let lastPremoveUci = ''; // Cached DOM queries (reduce overhead) let cachedGame = null; let cachedGameTimestamp = 0; const GAME_CACHE_TTL = 500; // Cache game object for 500ms // Cache board flip state (changes infrequently) let cachedBoardFlipped = false; let cachedFlipTimestamp = 0; // Main init async function init() { try { 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(); // observe board replacements (SPA safe) startAutoWatchers(); // game start/end watchers console.log('GabiBot: Initialized.'); } catch (error) { console.error('GabiBot Error:', error); alert('GabiBot: Could not find chess board. Please refresh or check console.'); } } // Build UI and bind settings async function buildUI() { // Create menu const menuWrap = document.createElement('div'); menuWrap.id = 'menuWrap'; const menuWrapStyle = document.createElement('style'); menuWrap.innerHTML = ` <div id="topText"> <a id="modTitle">♟ GabiBot</a> <button id="minimizeBtn" title="Minimize (Ctrl+B)">─</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: "✓"; 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: "✓"; 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); // Settings persistence const Settings = { save: debounce(() => { try { 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: { top: menuWrap.style.top, left: menuWrap.style.left } }; 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; } } }; 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'; } // Control binding helpers 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'); // Drag/move panel makePanelDraggable(menuWrap); // Minimize 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'); } }); ui = { menuWrap, setText(name, value, title) { const el = getElementByName(name, menuWrap); if (!el) return; const state = getStateElement(el); 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); updateEvaluationBar(BotState.currentEvaluation, playingAs); }, Settings }; // React to enable/disable or speed changes 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(); Object.keys(PositionCache).forEach(key => delete PositionCache[key]); 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); } // Board attach/detach function attachToBoard(boardEl) { // Invalidate cached game when board changes cachedGame = null; cachedGameTimestamp = 0; detachFromBoard(); // cleanup any previous if (!boardEl) { console.warn('GabiBot: No board element to attach.'); return; } // Ensure relative for overlay 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...'; ui.updateDisplay(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 { } } }; // Show ready ui.updateDisplay(pa()); if (BotState.hackEnabled) startTickLoop(); } function detachFromBoard() { if (!boardCtx) return; try { boardCtx.detachListeners(); } catch { } boardCtx = null; } function startDomBoardWatcher() { if (domObserver) try { domObserver.disconnect(); } catch { } domObserver = new MutationObserver(debounce(() => { // Look for a current board element 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 }); } // Game helpers const getBoard = () => boardCtx?.boardEl || qs('chess-board') || qs('.board'); const getGame = () => { const now = Date.now(); if (cachedGame && (now - cachedGameTimestamp) < GAME_CACHE_TTL) { return cachedGame; } cachedGame = getBoard()?.game || null; cachedGameTimestamp = now; return cachedGame; }; const getFen = (g) => { try { return g?.getFEN ? g.getFEN() : null; } catch { return null; } }; const getPlayerColor = (g) => { try { const v = g?.getPlayingAs?.(); return v === 2 ? 'b' : 'w'; } catch { return 'w'; } }; const getSideToMove = (g) => { const fen = getFen(g); return fen ? (fen.split(' ')[1] || null) : null; }; const isPlayersTurn = (g) => { const me = getPlayerColor(g), stm = getSideToMove(g); return !!me && !!stm && me === stm; }; const pa = () => (getGame()?.getPlayingAs ? getGame().getPlayingAs() : 1); function isBoardFlipped() { const now = Date.now(); if ((now - cachedFlipTimestamp) < 1000) 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; } // Arrow drawing function clearArrows() { if (!boardCtx) return; const { drawingBoard, ctx } = boardCtx; ctx.clearRect(0, 0, drawingBoard.width, drawingBoard.height); } 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(); } // Square to client XY 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 = getBoard(); 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 }; } // Minimal event sequences (fix: reduce excessive event firing) 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 { /* fallthrough */ } } el.dispatchEvent(new MouseEvent(type.replace('pointer', 'mouse'), { bubbles: true, cancelable: true, composed: true, ...opts })); } function getTargetAt(x, y) { return document.elementFromPoint(x, y) || getBoard() || 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 }; // ⚡ Reduced delays for bullet dispatchPointerOrMouse(startEl, usePointer ? 'pointerdown' : 'mousedown', downStart, usePointer); await sleep(10); // ⚡ 20 → 10ms 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(20); // ⚡ 40 → 20ms dispatchPointerOrMouse(endEl, usePointer ? 'pointerdown' : 'mousedown', downEnd, usePointer); await sleep(10); // ⚡ 20 → 10ms 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 simulateDragMove(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 down = { clientX: a.x, clientY: a.y, pointerId: 1, pointerType: 'mouse', isPrimary: true, buttons: 1 }; const up = { clientX: b.x, clientY: b.y, pointerId: 1, pointerType: 'mouse', isPrimary: true, buttons: 0 }; dispatchPointerOrMouse(startEl, usePointer ? 'pointerdown' : 'mousedown', down, usePointer); // Fewer move steps to avoid excessive firing const steps = 3; for (let i = 1; i <= steps; i++) { const t = i / steps; const mx = a.x + (b.x - a.x) * t; const my = a.y + (b.y - a.y) * t; dispatchPointerOrMouse(endEl, usePointer ? 'pointermove' : 'mousemove', { clientX: mx, clientY: my, buttons: 1 }, usePointer); await sleep(12); } dispatchPointerOrMouse(endEl, usePointer ? 'pointerup' : 'mouseup', up, usePointer); return true; } async function waitForFenChange(prevFen, timeout = 1000) { const start = Date.now(); while (Date.now() - start < timeout) { const g = getGame(); const fen = g?.getFEN ? g.getFEN() : null; if (fen && fen !== prevFen) return true; await sleep(40); } return false; } // Promotion handling 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, .promotion-card'); 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 < 1000) { 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()); // Only treat as success if the real board FEN changed const changed = await waitForFenChange(beforeFen, 1000); return !!changed; } // Engine integration 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 ? 100000 - s.mate : -100000 - s.mate; if (typeof s.cp === 'number') return s.cp; return -Infinity; } async function fetchEngineData(fen, depth, signal) { const startTime = performance.now(); console.log(`GabiBot: 📡 API request STARTED for FEN: ${fen.substring(0, 20)}... | Depth: ${depth}`); const call = async (params) => { const url = `${API_URL}?fen=${encodeURIComponent(fen)}&depth=${depth}&${params}`; const ctrl = new AbortController(); const onAbort = () => ctrl.abort('external-abort'); if (signal?.aborted) { ctrl.abort('already-aborted'); throw new DOMException('Aborted', 'AbortError'); } signal?.addEventListener('abort', onAbort, { once: true }); const to = setTimeout(() => ctrl.abort('timeout'), ANALYZE_TIMEOUT_MS); try { const res = await fetch(url, { method: 'GET', headers: { Accept: 'application/json' }, signal: ctrl.signal }); const endTime = performance.now(); const duration = endTime - startTime; if (!res.ok) { console.warn(`GabiBot: ❌ API failed (${res.status}) after ${duration.toFixed(0)}ms`); throw new Error(`API error ${res.status}`); } const data = await res.json(); if (data.success === false) { console.warn(`GabiBot: ❌ API success=false after ${duration.toFixed(0)}ms`); throw new Error('API success=false'); } console.log(`GabiBot: ✅ API success in ${duration.toFixed(0)}ms | FEN: ${fen.substring(0, 20)}...`); return data; } finally { clearTimeout(to); signal?.removeEventListener('abort', onAbort); } }; try { return await call(`multipv=${MULTIPV}&mode=analysis`); } catch { try { return await call(`multipv=${MULTIPV}&mode=bestmove`); } catch { return await call('mode=bestmove'); } } } async function fetchEngineDataWithRetry(fen, depth, signal, maxRetries = 1) { // Simple cache check if (PositionCache[fen]) { console.log('GabiBot: 🗃️ Using cached analysis'); return PositionCache[fen]; } let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { if (signal?.aborted || !BotState.hackEnabled) { console.log('GabiBot: ⏹️ Analysis aborted before attempt', attempt + 1); throw new DOMException('Aborted', 'AbortError'); } if (attempt > 0) { const backoff = Math.min(1000 * Math.pow(2, attempt - 1), 5000); console.log(`GabiBot: 🔁 Retry attempt #${attempt} for FEN: ${fen.substring(0, 20)}... (backoff: ${backoff}ms)`); await sleep(backoff); } try { const data = await fetchEngineData(fen, depth, signal); PositionCache[fen] = data; if (attempt > 0) { console.log(`GabiBot: 🎯 Retry succeeded on attempt #${attempt + 1}`); } return data; } catch (error) { lastError = error; console.warn(`GabiBot: ⚠️ Attempt #${attempt + 1} failed:`, error.message || error); if (attempt >= maxRetries) break; } } console.error(`GabiBot: 💥 All ${maxRetries + 1} attempts failed for FEN: ${fen.substring(0, 20)}...`); throw lastError; } 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 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') { // Neutral position when no eval whiteBar.style.height = '50%'; blackBar.style.height = '50%'; return; } if (evaluation.includes('M')) { const m = parseInt(evaluation.replace('M', '').replace('+', ''), 10); // Mate scores: positive = White mating, negative = Black mating score = m > 0 ? 10 : -10; // Cap at ±10 for mate } else { score = parseFloat(evaluation); } } else { score = parseFloat(evaluation); } if (isNaN(score)) { whiteBar.style.height = '50%'; blackBar.style.height = '50%'; return; } // Clamp score for visual representation const maxScore = 5; const clampedScore = Math.max(-maxScore, Math.min(maxScore, score)); // Calculate percentages (eval is ALWAYS from White's perspective) // +score = White winning, -score = Black winning const whitePercent = 50 + (clampedScore / maxScore) * 50; const blackPercent = 100 - whitePercent; // NO FLIPPING! The evaluation meaning stays constant. // White winning = more white, regardless of board orientation 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)'; } } // FEN helpers for piece info 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() }; } // En passant detection for premove capture mode 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]; } // Determine our planned move from PV given side-to-move function getOurMoveFromPV(pv, ourColor, sideToMove) { if (!pv) return null; const moves = pv.trim().split(/\s+/).filter(Boolean); if (!moves.length) return null; const idx = (sideToMove === ourColor) ? 0 : 1; return moves[idx] || null; } // Execute best move (draw + optional auto-move) async function executeAction(selectedUci, analysisFen) { try { clearArrows(); if (!selectedUci || selectedUci.length < 4) return; const from = selectedUci.substring(0, 2); const to = selectedUci.substring(2, 4); const promotionChar = selectedUci.length >= 5 ? selectedUci[4] : null; drawArrow(from, to, 'rgba(100, 255, 100, 0.7)', 4); if (BotState.hackEnabled && BotState.autoMove) { const game = getGame(); if (!game || !isPlayersTurn(game)) { BotState.statusInfo = 'Waiting for opponent...'; ui.updateDisplay(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 / 1000).toFixed(1)}s`; ui.updateDisplay(pa()); pendingMoveTimeoutId = setTimeout(async () => { const g = getGame(); if (!g) return; if (!isPlayersTurn(g)) { BotState.statusInfo = 'Move canceled (not our turn)'; ui.updateDisplay(pa()); return; } if (getFen(g) !== analysisFen) { BotState.statusInfo = 'Move canceled (position changed)'; ui.updateDisplay(pa()); return; } BotState.statusInfo = 'Making move...'; ui.updateDisplay(pa()); const success = await makeMove(from, to, analysisFen, promotionChar); BotState.statusInfo = success ? '✓ Move made!' : '❌ Move failed'; ui.updateDisplay(pa()); // Retry on failure if (!success) { setTimeout(() => { if (BotState.hackEnabled && isPlayersTurn(getGame())) { lastFenProcessedMain = ''; tick(); } }, 800); } }, totalDelay); } else { BotState.statusInfo = 'Ready (manual)'; ui.updateDisplay(pa()); } } catch (error) { console.error('Error in executeAction:', error); } } // Single queued analysis runner for both main and premove function scheduleAnalysis(kind, fen) { const analysisId = ++currentAnalysisId; analysisQueue = analysisQueue.then(async () => { if (analysisId !== currentAnalysisId) return; if (!BotState.hackEnabled) return; const game = getGame(); if (!game) return; if (kind === 'main') { if (lastFenProcessedMain === fen) return; } else { if (lastFenProcessedPremove === fen) return; } const ctrl = new AbortController(); // Local controller per analysis try { BotState.statusInfo = kind === 'main' ? '🔄 Analyzing...' : '🔄 Analyzing (premove)...'; ui.updateDisplay(pa()); const randomDepth = getRandomDepth(); // Check if newer analysis superseded this one if (analysisId !== currentAnalysisId) { ctrl.abort('superseded'); return; } const data = await fetchEngineDataWithRetry(fen, randomDepth, ctrl.signal); // Double-check still current after async operation if (analysisId !== currentAnalysisId) return; const best = parseBestLine(data); if (kind === 'main') { BotState.bestMove = best?.uci || '-'; BotState.currentEvaluation = scoreToDisplay(best?.score); BotState.principalVariation = best?.pv || 'Not available'; BotState.statusInfo = '✓ Ready'; ui.updateDisplay(pa()); if (best) await executeAction(best.uci, fen); lastFenProcessedMain = fen; } else { // Premove analysis const ourColor = getPlayerColor(game); const stm = getSideToMove(game); 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)'; ui.updateDisplay(pa()); lastFenProcessedPremove = fen; return; } if (!shouldPremove(ourUci, fen)) { BotState.statusInfo = `Premove skipped (${BotState.premoveMode})`; ui.updateDisplay(pa()); lastFenProcessedPremove = fen; return; } // 🛡️ SAFETY CHECK: Threat detection const safetyCheck = checkPremoveSafety(fen, ourUci, ourColor); if (!safetyCheck.safe) { BotState.statusInfo = `🛡️ Premove blocked: ${safetyCheck.reason}`; ui.updateDisplay(pa()); lastFenProcessedPremove = fen; return; } let currentChance = getEvalBasedPremoveChance(premoveEvalDisplay, ourColor); // 🛡️ Reduce confidence based on risk level if (safetyCheck.riskLevel > 0) { const riskPenalty = safetyCheck.riskLevel * 0.5; // 50% reduction at max risk currentChance = Math.max(5, currentChance - riskPenalty); console.log(`GabiBot: Risk detected (${safetyCheck.riskLevel}%), reducing confidence: ${currentChance.toFixed(0)}%`); } const chanceEl = qs('[name="premoveChance"] .itemState'); if (chanceEl) chanceEl.textContent = `${Math.round(currentChance)}%`; const roll = Math.random() * 100; if (roll > currentChance) { const skipReason = safetyCheck.riskLevel > 0 ? `${safetyCheck.reason} (${roll.toFixed(0)}% > ${currentChance.toFixed(0)}%)` : `eval: ${premoveEvalDisplay}, ${roll.toFixed(0)}% > ${currentChance.toFixed(0)}%`; BotState.statusInfo = `Premove skipped: ${skipReason}`; ui.updateDisplay(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 safetyEmoji = safetyCheck.riskLevel === 0 ? '✅' : safetyCheck.riskLevel < 25 ? '⚠️' : '🔶'; BotState.statusInfo = `${safetyEmoji} Premove: ${ourUci} (${Math.round(currentChance)}% confidence)`; ui.updateDisplay(pa()); lastFenProcessedPremove = fen; } } catch (error) { if (String(error?.name || error).toLowerCase().includes('abort') || String(error?.message || error).toLowerCase().includes('superseded')) { BotState.statusInfo = '⏸ Analysis canceled'; } else { console.error('GabiBot API Error:', error); BotState.statusInfo = '❌ API Error'; BotState.currentEvaluation = 'Error'; } ui.updateDisplay(pa()); } }); } // ═══════════════════════════════════════════════════════════ // PREMOVE SAFETY: HEURISTIC THREAT DETECTION // ═══════════════════════════════════════════════════════════ // Piece values for safety checks const PIECE_VALUES = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 0 }; // Get all squares attacking a given square (fast heuristic) function getAttackersOfSquare(fen, targetSquare, attackerColor) { const attackers = []; const placement = fen.split(' ')[0]; const ranks = placement.split('/'); const tFile = 'abcdefgh'.indexOf(targetSquare[0]); const tRank = parseInt(targetSquare[1], 10); if (tFile < 0 || tRank < 1 || tRank > 8) return attackers; // Helper: check square and add if it contains attacker piece const checkSquare = (file, rank, pieceTypes) => { if (file < 0 || file > 7 || rank < 1 || rank > 8) return; const sq = 'abcdefgh'[file] + rank; const ch = fenCharAtSquare(fen, sq); const piece = pieceFromFenChar(ch); if (piece && piece.color === attackerColor && pieceTypes.includes(piece.type)) { attackers.push({ square: sq, piece: piece.type }); } }; // Pawn attacks (diagonal) const pawnDir = attackerColor === 'w' ? 1 : -1; checkSquare(tFile - 1, tRank - pawnDir, ['p']); checkSquare(tFile + 1, tRank - pawnDir, ['p']); // Knight attacks const knightMoves = [ [2, 1], [2, -1], [-2, 1], [-2, -1], [1, 2], [1, -2], [-1, 2], [-1, -2] ]; knightMoves.forEach(([df, dr]) => checkSquare(tFile + df, tRank + dr, ['n'])); // King attacks 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']); } } // Sliding pieces (rook, bishop, queen) const directions = [ { dx: 1, dy: 0, pieces: ['r', 'q'] }, // right { dx: -1, dy: 0, pieces: ['r', 'q'] }, // left { dx: 0, dy: 1, pieces: ['r', 'q'] }, // up { dx: 0, dy: -1, pieces: ['r', 'q'] }, // down { dx: 1, dy: 1, pieces: ['b', 'q'] }, // diagonal { dx: 1, dy: -1, pieces: ['b', 'q'] }, { dx: -1, dy: 1, pieces: ['b', 'q'] }, { dx: -1, dy: -1, pieces: ['b', 'q'] } ]; directions.forEach(({ dx, dy, pieces }) => { let f = tFile + dx; let r = tRank + dy; while (f >= 0 && f <= 7 && r >= 1 && r <= 8) { const sq = 'abcdefgh'[f] + r; const ch = fenCharAtSquare(fen, sq); if (ch) { const piece = pieceFromFenChar(ch); if (piece && piece.color === attackerColor && pieces.includes(piece.type)) { attackers.push({ square: sq, piece: piece.type }); } break; // Blocked } f += dx; r += dy; } }); return attackers; } // Check if square is attacked by opponent function isSquareAttackedBy(fen, square, attackerColor) { return getAttackersOfSquare(fen, square, attackerColor).length > 0; } // Find king position function findKing(fen, color) { const placement = fen.split(' ')[0]; const ranks = placement.split('/'); const kingChar = color === 'w' ? 'K' : 'k'; for (let rankIdx = 0; rankIdx < 8; rankIdx++) { const rank = 8 - rankIdx; let file = 0; for (const ch of ranks[rankIdx]) { if (/\d/.test(ch)) { file += parseInt(ch, 10); } else { if (ch === kingChar) { return 'abcdefgh'[file] + rank; } file++; } } } return null; } // Simple FEN after move simulation (for king safety check) function makeSimpleMove(fen, from, to) { const parts = fen.split(' '); const placement = parts[0]; const ranks = placement.split('/'); const fromFile = 'abcdefgh'.indexOf(from[0]); const fromRank = parseInt(from[1], 10); const toFile = 'abcdefgh'.indexOf(to[0]); const toRank = parseInt(to[1], 10); if (fromFile < 0 || toFile < 0 || fromRank < 1 || toRank < 1) return fen; const fromRowIdx = 8 - fromRank; const toRowIdx = 8 - toRank; const movingPiece = fenCharAtSquare(fen, from); if (!movingPiece) return fen; // Remove piece from 'from' square let fromRow = ranks[fromRowIdx]; let fromCol = 0; let newFromRow = ''; let emptyCount = 0; for (const ch of fromRow) { if (/\d/.test(ch)) { const spaces = parseInt(ch, 10); if (fromCol + spaces > fromFile) { // Our square is in this empty span const before = fromFile - fromCol; const after = spaces - before - 1; if (before > 0) newFromRow += before; emptyCount = 1; // This square is now empty if (after > 0) newFromRow += after; fromCol += spaces; } else { newFromRow += ch; fromCol += spaces; } } else { if (fromCol === fromFile) { emptyCount = 1; // Replace piece with empty } else { if (emptyCount > 0) { newFromRow += emptyCount; emptyCount = 0; } newFromRow += ch; } fromCol++; } } if (emptyCount > 0) newFromRow += emptyCount; // Compress consecutive digits newFromRow = newFromRow.replace(/(\d)(\d)/g, (m, a, b) => parseInt(a) + parseInt(b)); ranks[fromRowIdx] = newFromRow; // Place piece on 'to' square let toRow = ranks[toRowIdx]; let toCol = 0; let newToRow = ''; for (const ch of toRow) { if (/\d/.test(ch)) { const spaces = parseInt(ch, 10); if (toCol + spaces > toFile) { const before = toFile - toCol; const after = spaces - before - 1; if (before > 0) newToRow += before; newToRow += movingPiece; // Place moving piece if (after > 0) newToRow += after; toCol += spaces; } else { newToRow += ch; toCol += spaces; } } else { if (toCol === toFile) { newToRow += movingPiece; // Replace whatever was there } else { newToRow += ch; } toCol++; } } ranks[toRowIdx] = newToRow; parts[0] = ranks.join('/'); return parts.join(' '); } // Main premove safety check function checkPremoveSafety(fen, uci, ourColor) { if (!fen || !uci || uci.length < 4) { return { safe: false, reason: 'Invalid move', riskLevel: 100 }; } 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 { safe: false, reason: 'Not our piece', riskLevel: 100 }; } const destCh = fenCharAtSquare(fen, to); const destPiece = pieceFromFenChar(destCh); let riskLevel = 0; const reasons = []; // Check 1: King safety (critical) if (movingPiece.type === 'k') { if (isSquareAttackedBy(fen, to, oppColor)) { return { safe: false, reason: 'King moves into check', riskLevel: 100 }; } } else { // Check if move exposes our king const newFen = makeSimpleMove(fen, from, to); const kingPos = findKing(newFen, ourColor); if (kingPos && isSquareAttackedBy(newFen, kingPos, oppColor)) { return { safe: false, reason: 'Exposes king to check', riskLevel: 100 }; } } // Check 2: Don't hang the queen if (movingPiece.type === 'q') { const attackers = getAttackersOfSquare(fen, to, oppColor); if (attackers.length > 0) { // Queen moving to attacked square const hasDefender = getAttackersOfSquare(fen, to, ourColor).length > 1; // >1 because queen itself if (!hasDefender || !destPiece) { return { safe: false, reason: 'Hangs queen', riskLevel: 90 }; } } } // Check 3: Don't hang rook for nothing if (movingPiece.type === 'r') { const attackers = getAttackersOfSquare(fen, to, oppColor); if (attackers.length > 0) { const captureValue = destPiece ? PIECE_VALUES[destPiece.type] : 0; if (captureValue < PIECE_VALUES.r) { const hasDefender = getAttackersOfSquare(fen, to, ourColor).length > 1; if (!hasDefender) { reasons.push('Hangs rook'); riskLevel += 60; } } } } // Check 4: Destination square safety const destAttackers = getAttackersOfSquare(fen, to, oppColor); if (destAttackers.length > 0 && !destPiece) { // Moving to attacked empty square const defenders = getAttackersOfSquare(fen, to, ourColor).length; if (defenders === 0) { reasons.push('Moves to undefended attacked square'); riskLevel += 30; } else if (destAttackers.length > defenders) { reasons.push('Moves to heavily attacked square'); riskLevel += 20; } } // Check 5: Unfavorable trades if (destPiece && destPiece.color === oppColor) { const ourValue = PIECE_VALUES[movingPiece.type]; const theirValue = PIECE_VALUES[destPiece.type]; const destAttackers = getAttackersOfSquare(fen, to, oppColor); if (destAttackers.length > 0 && ourValue > theirValue) { reasons.push(`Bad trade: ${movingPiece.type} for ${destPiece.type}`); riskLevel += 25; } } const safe = riskLevel < 50; const reason = reasons.length > 0 ? reasons.join(', ') : (safe ? 'Move appears safe' : 'Move risky'); return { safe, reason, riskLevel }; } // Should we premove this UCI for the given FEN (mode-aware) 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 fromCh = fenCharAtSquare(fen, from); const toCh = fenCharAtSquare(fen, to); const fromPiece = pieceFromFenChar(fromCh); const toPiece = pieceFromFenChar(toCh); 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; } // Calculate premove confidence based on position evaluation function getEvalBasedPremoveChance(evaluation, ourColor) { if (!BotState.premoveEnabled) return 0; const game = getGame(); if (!game || isPlayersTurn(game)) 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)) { // Mate is always from White's perspective from engine // Positive mate = White winning, Negative = Black winning const ourMate = ourColor === 'w' ? mateNum : -mateNum; return ourMate > 0 ? 100 : 20; } } evalScore = parseFloat(evaluation); } else { evalScore = parseFloat(evaluation); } if (isNaN(evalScore)) return 0; // Engine eval is from White's perspective // Convert to our perspective const ourEval = ourColor === 'w' ? evalScore : -evalScore; if (ourEval >= 3.0) return 90; if (ourEval >= 2.0) return 75; if (ourEval >= 1.0) return 50; if (ourEval >= 0.5) return 35; if (ourEval >= 0) return 25; return 20; } // Tick loop function tick() { if (!BotState.hackEnabled) return; 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(); lastPremoveFen = ''; lastPremoveUci = ''; } if (isPlayersTurn(game)) { if (lastFenProcessedMain !== fen) { scheduleAnalysis('main', fen); } } else { if (BotState.premoveEnabled) { if (lastFenProcessedPremove !== fen) { scheduleAnalysis('premove', fen); } else { // Pass ourColor when updating premove chance display const chanceEl = qs('[name="premoveChance"] .itemState'); if (chanceEl && BotState.currentEvaluation && BotState.currentEvaluation !== '-') { const ourColor = getPlayerColor(game); const currentChance = getEvalBasedPremoveChance(BotState.currentEvaluation, ourColor); chanceEl.textContent = `${Math.round(currentChance)}%`; } BotState.statusInfo = (lastPremoveUci && lastPremoveFen === 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 startTickLoop() { stopTickLoop(); const interval = Math.max(150, 1100 - (Number(BotState.updateSpeed) || 8) * 100); tickTimer = setInterval(tick, interval); tick(); } function stopTickLoop() { if (tickTimer) clearInterval(tickTimer); tickTimer = null; } // Game auto-end detection and auto-rematch watchers 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, game started'); const btn = qsa('button', modal).find(b => /rematch/i.test((b.textContent || '').trim()) || /rematch/i.test((b.getAttribute?.('aria-label') || '').trim()) ); if (btn) { console.log('GabiBot: [2s] ✅ Clicking Rematch'); btn.click(); } else { console.log('GabiBot: [2s] No Rematch button'); } }, 2000); setTimeout(() => { const modal = qs('.game-over-modal-content'); if (!modal) return console.log('GabiBot: [12s] Modal closed, game started'); const btn = qsa('button', modal).find(b => { const text = (b.textContent || '').replace(/\s+/g, ' ').trim(); return /new.*\d+.*min/i.test(text); }); if (btn) { console.log(`GabiBot: [12s] ✅ Clicking "${btn.textContent.trim()}"`); btn.click(); } else { console.log('GabiBot: [12s] No "New X min" button'); } }, 12000); setTimeout(async () => { const modal = qs('.game-over-modal-content'); if (!modal) return console.log('GabiBot: [22s] Modal closed, game started'); console.log('GabiBot: [22s] Using New Game tab fallback'); 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) { console.log('GabiBot: [22s] Clicking New Game tab'); tab.click(); await sleep(400); const startBtn = qsa('button').find(b => /start.*game/i.test((b.textContent || '').trim()) ); if (startBtn) { console.log('GabiBot: [22s] ✅ Clicking Start Game'); startBtn.click(); } else { console.log('GabiBot: [22s] ❌ Start Game not found'); } } else { console.log('GabiBot: [22s] ❌ New Game tab not found'); } }, 22000); } } if (!gameOverModal && gameEndDetected) { console.log('GabiBot: New game started, bot analyzing...'); gameEndDetected = false; if (BotState.hackEnabled) { BotState.statusInfo = 'Ready'; ui?.updateDisplay(pa()); setTimeout(() => { if (BotState.hackEnabled) tick(); }, 500); } } }, 1000); } // Draggable panel 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 { ui?.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); } // Kick off setTimeout(init, 3000); })();