Chess.com cheat engine — K-EXPERT edition
// ==UserScript==
// @name Chess.com Cheat Engine
// @namespace http://tampermonkey.net/
// @version 12.0
// @description Chess.com cheat engine — K-EXPERT edition
// @author kliel
// @license MIT
// @match https://www.chess.com/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect unpkg.com
// @connect lichess.org
// @connect explorer.lichess.ovh
// @run-at document-idle
// @grant unsafeWindow
// ==/UserScript==
(function () {
'use strict';
// ─── PAGE GUARD ─────────────────────────────────────────────────────────────
const INACTIVE_PATHS = [/\/analysis/, /\/learn/, /\/puzzles/, /\/lessons/, /\/drills/, /\/courses/, /\/practice/, /\/openings/];
const isInactivePage = () => INACTIVE_PATHS.some(p => p.test(location.pathname));
let _paused = isInactivePage();
(['pushState', 'replaceState']).forEach(m => {
const orig = history[m];
history[m] = function (...a) { const r = orig.apply(this, a); window.dispatchEvent(new Event('_chess_nav')); return r; };
});
window.addEventListener('popstate', () => window.dispatchEvent(new Event('_chess_nav')));
window.addEventListener('_chess_nav', () => {
const was = _paused;
_paused = isInactivePage();
if (_paused && !was) {
try { SF.worker && SF.worker.postMessage('stop'); } catch (_) {}
UI.clearArrows();
}
if (!_paused && was) {
State.lastFen = null;
State.boardCache = null;
setTimeout(Loop.tick, 200);
}
});
// ─── CONFIG ──────────────────────────────────────────────────────────────────
const CFG = {
active: false,
autoPlay: false,
useBook: true,
showThreats: true,
depth: 14,
multiPV: 5,
humanization: true,
suboptimalRate: 0.35,
correlation: 0.60,
timeControl: 'blitz',
// min/max = auto-play delay in ms | depth = SF search depth
timing: {
bullet: { min: 200, max: 700, depth: 8 },
blitz: { min: 1000, max: 4000, depth: 12 },
rapid: { min: 4000, max: 12000, depth: 16 },
},
};
// ─── STATE ───────────────────────────────────────────────────────────────────
const State = {
lastFen: null,
playerColor: null,
moveCount: 0,
boardCache: null,
candidates: {},
currentEval: null,
opponentMove: null,
bestCount: 0,
totalCount: 0,
perfectStreak: 0,
sloppyStreak: 0,
};
// ─── UTILS ───────────────────────────────────────────────────────────────────
const $ = (sel, root = document) => {
try {
if (Array.isArray(sel)) { for (const s of sel) { const e = root.querySelector(s); if (e) return e; } return null; }
return root.querySelector(sel);
} catch { return null; }
};
const sleep = ms => new Promise(r => setTimeout(r, ms));
const rnd = (a, b) => Math.random() * (b - a) + a;
const gauss = (m, s) => { const u = Math.random(), v = Math.random(); return m + Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v) * s; };
const humanMs = (min, max) => {
// Average of 3 uniforms = triangular-bell shape, strictly between min and max
const r = (Math.random() + Math.random() + Math.random()) / 3;
return Math.round(min + r * (max - min));
};
const log = (m, t) => console.log('%c[KE] ' + m, 'color:' + ({info:'#4caf50',warn:'#ffcc00',error:'#f44336',debug:'#90caf9'})[t||'info'] + ';font-weight:bold');
// ─── BOARD / GAME ─────────────────────────────────────────────────────────────
const Board = {
el: () => {
if (State.boardCache && State.boardCache.isConnected) return State.boardCache;
State.boardCache = $(['wc-chess-board', 'chess-board']);
return State.boardCache;
},
color: () => {
try {
const b = Board.el();
return (b && (b.classList.contains('flipped') || b.getAttribute('flipped') === 'true')) ? 'b' : 'w';
} catch (e) { return 'w'; }
},
fen: () => {
try {
const b = Board.el();
if (!b) return null;
if (b.game && b.game.getFEN) return b.game.getFEN();
const rk = Object.keys(b).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternal'));
if (rk) {
let cur = b[rk], d = 0;
while (cur && d++ < 150) {
if (cur.memoizedProps && cur.memoizedProps.game && cur.memoizedProps.game.fen) return cur.memoizedProps.game.fen;
if (typeof cur.memoizedProps === 'object' && cur.memoizedProps !== null && typeof cur.memoizedProps.fen === 'string') return cur.memoizedProps.fen;
cur = cur.return;
}
}
return null;
} catch (e) { return null; }
},
myTurn: (fen) => fen && State.playerColor && fen.split(' ')[1] === State.playerColor,
coords: (sq) => {
try {
const b = Board.el();
if (!b) return null;
const r = b.getBoundingClientRect();
if (!r || !r.width) return null;
const sqSz = r.width / 8;
const flip = State.playerColor === 'b';
const f = sq.charCodeAt(0) - 97;
const rk = parseInt(sq[1]) - 1;
return {
x: r.left + (flip ? 7 - f : f) * sqSz + sqSz / 2,
y: r.top + (flip ? rk : 7 - rk) * sqSz + sqSz / 2
};
} catch (e) { return null; }
},
};
// ─── OPENING BOOK ─────────────────────────────────────────────────────────────
const Book = {
fetch: (fen) => {
if (!CFG.useBook) return Promise.resolve(null);
return new Promise(res => {
const t = setTimeout(() => res(null), 2000);
GM_xmlhttpRequest({
method: 'GET',
url: 'https://explorer.lichess.ovh/masters?fen=' + encodeURIComponent(fen),
onload: r => {
clearTimeout(t);
try {
const d = JSON.parse(r.responseText);
if (!d.moves || !d.moves.length) return res(null);
const top = d.moves.slice(0, 3);
const total = top.reduce((s, m) => s + m.white + m.draw + m.black, 0);
let x = Math.random() * total;
for (const m of top) {
x -= m.white + m.draw + m.black;
if (x <= 0) return res(m.uci);
}
res(top[0].uci);
} catch (e) { res(null); }
},
onerror: () => { clearTimeout(t); res(null); }
});
});
},
};
// ─── STOCKFISH ───────────────────────────────────────────────────────────────
const SF = {
worker: null,
ready: false,
_analyzing: false,
_pendingFen: null,
_resolveInit: null,
init: () => new Promise(resolve => {
if (SF.ready) return resolve(true);
log('Loading Stockfish...');
UI.setStatus('loading');
GM_xmlhttpRequest({
method: 'GET',
url: 'https://unpkg.com/[email protected]/stockfish.js',
onload: res => {
try {
const blob = new Blob([res.responseText], { type: 'application/javascript' });
SF.worker = new Worker(URL.createObjectURL(blob));
SF.worker.onerror = err => { log('SF worker error: ' + (err && err.message), 'error'); UI.setStatus('error'); };
SF.worker.onmessage = e => SF._msg(e.data);
SF._resolveInit = resolve;
SF.worker.postMessage('uci');
} catch (e) { log('SF init failed: ' + e, 'error'); UI.setStatus('error'); resolve(false); }
},
onerror: () => { log('SF download failed', 'error'); UI.setStatus('error'); resolve(false); }
});
}),
_msg: (msg) => {
if (msg === 'uciok') {
SF.worker.postMessage('setoption name MultiPV value ' + CFG.multiPV);
SF.worker.postMessage('isready');
return;
}
if (msg === 'readyok') {
SF.ready = true;
log('Stockfish ready');
UI.setStatus('ready');
if (SF._resolveInit) { SF._resolveInit(true); SF._resolveInit = null; }
// If we were already activated before engine was ready, fire immediately
if (CFG.active && !_paused) { State.lastFen = null; Loop.tick(); }
return;
}
if (msg.startsWith('info') && msg.includes(' score ') && SF._analyzing) {
SF._parseInfo(msg);
return;
}
if (msg.startsWith('bestmove') && SF._analyzing) {
SF._analyzing = false;
const parts = msg.split(' ');
const move = parts[1];
if (move && move !== '(none)') Engine.onBestMove(move);
// Drain any queued position
if (SF._pendingFen) {
const fen = SF._pendingFen;
SF._pendingFen = null;
SF._go(fen);
}
}
},
_parseInfo: (msg) => {
try {
const score = msg.match(/score (cp|mate) (-?\d+)/);
const pv = msg.match(/multipv (\d+)/);
const mv = msg.match(/ pv ([a-h][1-8][a-h][1-8]\w*)/);
const dep = msg.match(/depth (\d+)/);
if (!score || !pv || !mv) return;
const mpv = parseInt(pv[1]);
const type = score[1];
let val = parseInt(score[2]);
if (type === 'cp') val = val / 100;
State.candidates[mpv] = { move: mv[1], eval: { type: type, val: val }, depth: parseInt((dep && dep[1]) || 0) };
if (mpv === 1) {
State.currentEval = { type: type, val: val };
const pvStr = msg.split(' pv ')[1];
if (pvStr) { const pvMoves = pvStr.split(' '); State.opponentMove = pvMoves[1] || null; }
UI.updateEval(type, val);
}
} catch (e) { /* ignore */ }
},
_go: (fen) => {
if (!SF.ready || !SF.worker) return;
SF._analyzing = true;
State.candidates = {};
State.currentEval = null;
SF.worker.postMessage('stop');
SF.worker.postMessage('position fen ' + fen);
SF.worker.postMessage('setoption name MultiPV value ' + CFG.multiPV);
SF.worker.postMessage('go depth ' + CFG.depth);
},
analyze: (fen) => {
if (!SF.ready || !SF.worker) return;
if (SF._analyzing) {
SF._pendingFen = fen;
SF.worker.postMessage('stop'); // triggers bestmove -> drains pending
} else {
SF._go(fen);
}
},
stop: () => {
SF._pendingFen = null;
SF._analyzing = false;
try { SF.worker && SF.worker.postMessage('stop'); } catch (e) { /* ignore */ }
},
};
// ─── HUMANIZATION ────────────────────────────────────────────────────────────
const Human = {
phase: (fen) => {
if (!fen) return 'mid';
const mn = parseInt(fen.split(' ')[5] || 1);
const pieces = (fen.split(' ')[0].match(/[rnbqRNBQ]/g) || []).length;
if (mn <= 10) return 'open';
if (pieces <= 6) return 'end';
return 'mid';
},
pickMove: (bestMove) => {
if (!CFG.humanization) return { move: bestMove, best: true };
const best = State.candidates[1];
if (!best) return { move: bestMove, best: true };
// Streak guards
if (State.perfectStreak >= 4) { const s = Human._subopt(); if (s) return s; }
if (State.sloppyStreak >= 3) return { move: bestMove, best: true };
// Dynamic rate
let rate = CFG.suboptimalRate;
if (State.totalCount >= 6) {
const corr = State.bestCount / State.totalCount;
if (corr > CFG.correlation + 0.08) rate += 0.15;
else if (corr < CFG.correlation - 0.12) rate *= 0.2;
}
const ev = (State.currentEval && State.currentEval.val) || 0;
if (ev > 2) rate += 0.12;
if (ev > 4) rate += 0.15;
if (ev > 6) rate += 0.20;
if (ev < -1) rate *= 0.3;
rate = Math.max(0.05, Math.min(0.65, rate));
if (Math.random() < rate) {
const s = Human._subopt();
if (s) return s;
}
return { move: bestMove, best: true };
},
_subopt: () => {
const best = State.candidates[1];
if (!best || Object.keys(State.candidates).length < 2) return null;
const maxLoss = ({ open: 50, mid: 100, end: 60 })[Human.phase(State.lastFen)] || 80;
const alts = [];
for (let i = 2; i <= CFG.multiPV; i++) {
const c = State.candidates[i];
if (!c || !c.eval) continue;
let loss;
if (best.eval.type === 'mate') { loss = 999; }
else if (c.eval.type === 'mate' && c.eval.val > 0) { loss = 0; }
else { loss = (best.eval.val - c.eval.val) * 100; }
if (loss >= 0 && loss <= maxLoss) alts.push({ move: c.move, loss: loss });
}
if (!alts.length) return null;
const w = alts.map(a => 1 / (1 + a.loss / 25));
const tot = w.reduce((s, x) => s + x, 0);
let r = Math.random() * tot;
for (let i = 0; i < alts.length; i++) {
r -= w[i];
if (r <= 0) return { move: alts[i].move, best: false };
}
return { move: alts[0].move, best: false };
},
track: (best) => {
State.totalCount++;
if (best) { State.bestCount++; State.perfectStreak++; State.sloppyStreak = 0; }
else { State.perfectStreak = 0; State.sloppyStreak++; }
},
delay: () => {
const tc = CFG.timing[CFG.timeControl] || CFG.timing.blitz;
// Also keep depth in sync (in case delay is called before a TC-switch fires analyze)
if (tc.depth && CFG.depth !== tc.depth) CFG.depth = tc.depth;
return humanMs(tc.min, tc.max);
},
};
// ─── ENGINE COORDINATOR ──────────────────────────────────────────────────────
const Engine = {
onBestMove: async (bestMove, isBook) => {
if (_paused || !CFG.active) return;
State.moveCount++;
const picked = isBook ? { move: bestMove, best: true } : Human.pickMove(bestMove);
Human.track(picked.best);
UI.drawArrows(bestMove, picked.move);
UI.updateMove(picked.move, picked.best);
if (CFG.autoPlay && Board.myTurn(State.lastFen)) {
const delay = Human.delay();
log('Auto-play in ' + Math.round(delay) + 'ms');
await sleep(delay);
// After sleeping, re-read the live FEN — State.lastFen may have advanced
const liveFen = Board.fen();
if (!_paused && CFG.active && liveFen && Board.myTurn(liveFen)) {
await Mover.exec(picked.move);
}
}
},
};
// ─── MAIN LOOP ───────────────────────────────────────────────────────────────
const Loop = {
_lastAnalyzedFen: null,
tick: () => {
if (_paused || !CFG.active) return;
try {
const fen = Board.fen();
if (!fen) return;
State.playerColor = Board.color();
// New game detection
const isStart = fen.startsWith('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR');
if (isStart && State.moveCount > 2) {
State.moveCount = 0; State.bestCount = 0; State.totalCount = 0;
State.perfectStreak = 0; State.sloppyStreak = 0;
Loop._lastAnalyzedFen = null;
}
if (fen === State.lastFen) return;
State.lastFen = fen;
UI.clearArrows();
UI.updateMove('...', true);
if ($(['.game-over-modal-container', '.modal-game-over-component', '[data-cy="game-over-modal"]'])) return;
if (Board.myTurn(fen) && fen !== Loop._lastAnalyzedFen) {
Loop._lastAnalyzedFen = fen;
if (CFG.useBook) {
Book.fetch(fen).then(bm => {
if (!CFG.active || _paused || fen !== State.lastFen) return;
if (bm) { log('Book: ' + bm); Engine.onBestMove(bm, true); }
else SF.analyze(fen);
});
} else {
SF.analyze(fen);
}
}
} catch (e) { log('loop error: ' + e.message, 'warn'); }
},
start: () => {
const target = $(['vertical-move-list', 'wc-move-list', '.move-list-component']) || document.body;
new MutationObserver(() => {
if (!_paused && CFG.active) requestAnimationFrame(Loop.tick);
}).observe(target, { childList: true, subtree: true, characterData: true });
setInterval(() => { if (!_paused && CFG.active) Loop.tick(); }, 600);
},
};
// ─── MOVE EXECUTOR ───────────────────────────────────────────────────────────
const Mover = {
exec: async (move) => {
try {
// Use live board FEN — stored State.lastFen may be stale after a long delay
const liveFen = Board.fen();
if (!liveFen || !Board.myTurn(liveFen)) return;
const from = move.slice(0, 2);
const to = move.slice(2, 4);
const promo = move[4] || 'q';
await Mover.drag(from, to);
if ((to[1] === '8' || to[1] === '1') && Mover.isPawn(from)) await Mover.promo(promo);
} catch (e) { log('exec error: ' + e.message, 'warn'); }
},
isPawn: (sq) => {
try {
const b = Board.el();
if (!b) return false;
const coord = (sq.charCodeAt(0) - 96) + '' + sq[1];
const f = b.querySelector('.piece.square-' + coord);
if (f) return f.className.includes('wp') || f.className.includes('bp');
return sq[1] === '2' || sq[1] === '7';
} catch (e) { return false; }
},
drag: async (from, to) => {
const p1 = Board.coords(from);
const p2 = Board.coords(to);
if (!p1 || !p2) return;
const b = Board.el();
const opts = { bubbles: true, composed: true, buttons: 1, pointerId: 1, isPrimary: true };
const pe = (t, x, y) => new PointerEvent(t, Object.assign({}, opts, { clientX: x, clientY: y }));
const me = (t, x, y) => new MouseEvent(t, Object.assign({}, opts, { clientX: x, clientY: y }));
const fromCoord = (p1.x + '').slice(0, 3); // not used for selector, just drag
const src = document.elementFromPoint(p1.x, p1.y) || b;
src.dispatchEvent(pe('pointerdown', p1.x, p1.y));
src.dispatchEvent(me('mousedown', p1.x, p1.y));
await sleep(rnd(25, 65));
const steps = 10 + Math.round(rnd(0, 6));
const cpx = (p1.x + p2.x) / 2 + gauss(0, 18);
const cpy = (p1.y + p2.y) / 2 + gauss(0, 18);
for (let i = 1; i <= steps; i++) {
const t = i / steps;
const x = (1-t)*(1-t)*p1.x + 2*(1-t)*t*cpx + t*t*p2.x + gauss(0, 1.2);
const y = (1-t)*(1-t)*p1.y + 2*(1-t)*t*cpy + t*t*p2.y + gauss(0, 1.2);
document.dispatchEvent(pe('pointermove', x, y));
document.dispatchEvent(me('mousemove', x, y));
if (Math.random() < 0.35) await sleep(rnd(2, 8));
}
const dst = document.elementFromPoint(p2.x, p2.y) || b;
dst.dispatchEvent(pe('pointerup', p2.x, p2.y));
dst.dispatchEvent(me('mouseup', p2.x, p2.y));
dst.dispatchEvent(pe('click', p2.x, p2.y));
},
promo: async (piece) => {
piece = piece || 'q';
await sleep(200);
const idx = { q: 0, r: 1, b: 2, n: 3 }[piece] || 0;
const sels = [
'.promotion-piece[data-piece="' + piece + '"]',
'.promotion-window .promotion-piece:nth-child(' + (idx+1) + ')',
'.promotion-piece',
];
let el = null;
for (let i = 0; i < 15 && !el; i++) {
await sleep(80);
for (const s of sels) { try { el = document.querySelector(s); } catch(e){} if (el) break; }
}
if (el) { el.click(); log('Promoted to ' + piece); }
},
};
// ─── UI ──────────────────────────────────────────────────────────────────────
const UI = {
panel: null,
_stealth: false,
init: () => {
UI._css();
UI._build();
},
_css: () => {
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap');
.ke-panel {
position: fixed; top: 60px; left: 60px; z-index: 99999;
width: 272px;
background: #0d0d10;
border: 1px solid rgba(255,255,255,0.07);
border-radius: 12px;
font-family: 'Inter', sans-serif;
box-shadow: 0 24px 64px rgba(0,0,0,0.75), 0 0 0 1px rgba(255,255,255,0.03);
color: #e0e0e0;
touch-action: none;
user-select: none;
transition: opacity 0.2s;
}
.ke-panel.stealth { opacity: 0 !important; pointer-events: none; }
/* Header */
.ke-hdr {
padding: 13px 14px 11px;
display: flex; align-items: center; gap: 9px;
cursor: grab; border-bottom: 1px solid rgba(255,255,255,0.06);
background: linear-gradient(180deg,rgba(255,255,255,0.025) 0%,transparent 100%);
border-radius: 12px 12px 0 0;
}
.ke-hdr:active { cursor: grabbing; }
.ke-logo {
font-weight: 800; font-size: 12.5px; letter-spacing: 0.1em; flex: 1;
background: linear-gradient(120deg,#69f0ae,#4caf50); -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.ke-logo em { -webkit-text-fill-color: rgba(255,255,255,0.35); font-style: normal; font-weight: 400; }
.ke-dot {
width: 6px; height: 6px; border-radius: 50%; background: #333;
transition: background 0.3s, box-shadow 0.3s; flex-shrink: 0;
}
.ke-dot.loading { background: #ffc107; animation: ke-pulse 0.7s ease-in-out infinite; }
.ke-dot.ready { background: #444; }
.ke-dot.active { background: #4caf50; box-shadow: 0 0 7px #4caf50; animation: ke-pulse 1.8s ease-in-out infinite; }
.ke-dot.error { background: #f44336; }
.ke-colbtn {
width: 22px; height: 22px; border-radius: 5px; background: rgba(255,255,255,0.05);
border: none; color: #555; cursor: pointer; font-size: 13px;
display: flex; align-items: center; justify-content: center;
transition: 0.15s; flex-shrink: 0; line-height: 1;
}
.ke-colbtn:hover { background: rgba(255,255,255,0.1); color: #ccc; }
/* Body collapse */
.ke-body { overflow: hidden; }
.ke-body.col { max-height: 0 !important; overflow: hidden; }
/* Master button */
.ke-master {
margin: 12px 12px 10px;
padding: 11px 13px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.07);
background: #141418;
cursor: pointer;
display: flex; align-items: center; gap: 10px;
transition: border-color 0.2s, box-shadow 0.2s;
position: relative; overflow: hidden;
}
.ke-master-bg {
position: absolute; inset: 0;
background: linear-gradient(120deg,#2e7d32,#43a047);
opacity: 0; transition: opacity 0.2s; pointer-events: none;
}
.ke-master.on .ke-master-bg { opacity: 1; }
.ke-master.on { border-color: #4caf50; box-shadow: 0 0 18px rgba(76,175,80,0.28); }
.ke-master:hover { border-color: rgba(255,255,255,0.13); }
.ke-mic {
width: 30px; height: 30px; border-radius: 50%;
background: rgba(255,255,255,0.07);
display: flex; align-items: center; justify-content: center;
font-size: 13px; position: relative; z-index: 1; flex-shrink: 0;
}
.ke-master.on .ke-mic { background: rgba(255,255,255,0.14); }
.ke-mtxt { position: relative; z-index: 1; }
.ke-mtitle { font-size: 11.5px; font-weight: 700; letter-spacing: 0.07em; color: #555; transition: color 0.2s; }
.ke-master.on .ke-mtitle { color: #fff; }
.ke-msub { font-size: 10px; color: #444; margin-top: 1px; transition: color 0.2s; }
.ke-master.on .ke-msub { color: rgba(255,255,255,0.65); }
/* Eval row */
.ke-eval-row {
margin: 0 12px 10px;
background: #111115; border: 1px solid rgba(255,255,255,0.06);
border-radius: 8px; padding: 11px 13px;
display: flex; align-items: center; justify-content: space-between;
}
.ke-eval {
font-family: 'JetBrains Mono',monospace; font-size: 28px; font-weight: 600;
color: #333; transition: color 0.2s; line-height: 1;
}
.ke-eval.pos { color: #4caf50; }
.ke-eval.neg { color: #f44336; }
.ke-eval.neu { color: #e0e0e0; }
.ke-movebox { text-align: right; }
.ke-movelbl { font-size: 9px; color: #444; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 4px; }
.ke-move {
font-family: 'JetBrains Mono',monospace; font-size: 15px; font-weight: 600;
color: #4caf50; background: rgba(76,175,80,0.1);
padding: 3px 8px; border-radius: 5px; letter-spacing: 0.05em;
transition: color 0.15s, background 0.15s;
}
.ke-move.sub { color: #ffc107; background: rgba(255,193,7,0.1); }
.ke-move.idle { color: #333; background: transparent; }
/* Tabs */
.ke-tabbar {
display: flex; margin: 0 12px 10px;
background: #111115; padding: 3px; border-radius: 7px;
border: 1px solid rgba(255,255,255,0.05); gap: 3px;
}
.ke-tab {
flex: 1; padding: 6px 0; font-size: 9.5px; font-weight: 600;
letter-spacing: 0.07em; color: #444; cursor: pointer;
border-radius: 5px; text-align: center; transition: 0.15s;
}
.ke-tab:hover { color: #999; }
.ke-tab.act { background: #1a1a20; color: #e0e0e0; box-shadow: 0 1px 4px rgba(0,0,0,0.5); }
/* Pages */
.ke-page { display: none; padding: 0 12px 12px; }
.ke-page.act { display: block; }
/* Row */
.ke-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 9px; }
.ke-rlbl { font-size: 11px; color: #888; }
/* Toggle switch */
.ke-sw {
width: 32px; height: 17px; border-radius: 9px;
background: #1e1e24; border: 1px solid rgba(255,255,255,0.07);
cursor: pointer; position: relative; transition: 0.18s; flex-shrink: 0;
}
.ke-sw.on { background: #4caf50; border-color: #4caf50; }
.ke-sw::after {
content:''; position: absolute; top: 2px; left: 2px;
width: 11px; height: 11px; border-radius: 50%;
background: #fff; transition: transform 0.18s;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.ke-sw.on::after { transform: translateX(15px); }
/* TC buttons */
.ke-tc-row { display: flex; gap: 5px; margin-bottom: 10px; }
.ke-tc {
flex: 1; padding: 5px 0; font-size: 9.5px; font-weight: 600;
border-radius: 5px; cursor: pointer; text-align: center;
background: #141418; border: 1px solid rgba(255,255,255,0.06);
color: #444; transition: 0.15s;
}
.ke-tc:hover { color: #ccc; border-color: rgba(255,255,255,0.13); }
.ke-tc.act { color: #111; font-weight: 700; border-color: transparent; }
.ke-tc[data-tc=bullet].act { background: #ef5350; }
.ke-tc[data-tc=blitz].act { background: #ffc107; }
.ke-tc[data-tc=rapid].act { background: #4caf50; }
/* Sliders */
.ke-slwrap { margin-bottom: 11px; }
.ke-slhdr { display: flex; justify-content: space-between; margin-bottom: 4px; }
.ke-slhdr span:first-child { font-size: 10.5px; color: #777; }
.ke-slhdr span:last-child { font-size: 10px; color: #4caf50; font-family: 'JetBrains Mono',monospace; }
.ke-sl {
-webkit-appearance: none; width: 100%; height: 3px;
border-radius: 2px; outline: none; cursor: pointer;
background: linear-gradient(90deg,#4caf50 var(--v,50%),#1e1e24 var(--v,50%));
}
.ke-sl::-webkit-slider-thumb {
-webkit-appearance: none; width: 13px; height: 13px;
background: #fff; border-radius: 50%; cursor: pointer;
box-shadow: 0 0 0 3px rgba(76,175,80,0.25), 0 2px 5px rgba(0,0,0,0.4);
}
/* Stats */
.ke-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 2px; }
.ke-stat {
background: #111115; border: 1px solid rgba(255,255,255,0.05);
border-radius: 7px; padding: 8px 10px;
}
.ke-stat-l { font-size: 9px; color: #444; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 3px; }
.ke-stat-v { font-family: 'JetBrains Mono',monospace; font-size: 17px; font-weight: 600; color: #ddd; }
/* Footer */
.ke-footer {
padding: 7px 12px; border-top: 1px solid rgba(255,255,255,0.05);
display: flex; gap: 10px; font-size: 9.5px; color: #444;
}
.ke-k {
background: #141418; border: 1px solid rgba(255,255,255,0.09);
border-radius: 3px; padding: 1px 5px;
font-family: 'JetBrains Mono',monospace; font-size: 9px;
}
/* Arrow overlay */
.ke-overlay {
position: fixed; top: 0; left: 0;
pointer-events: none; z-index: 9998;
transition: opacity 0.2s;
}
@keyframes ke-pulse { 0%,100%{opacity:1} 50%{opacity:0.35} }
`);
},
_build: () => {
if (UI.panel) return;
const p = document.createElement('div');
p.className = 'ke-panel';
p.innerHTML = `
<div class="ke-hdr">
<div class="ke-logo">K-EXPERT<em>.MENU</em></div>
<div class="ke-dot ready" id="ke-dot"></div>
<button class="ke-colbtn" id="ke-col">−</button>
</div>
<div class="ke-body" id="ke-body">
<div class="ke-master" id="ke-master">
<div class="ke-master-bg"></div>
<div class="ke-mic">⚡</div>
<div class="ke-mtxt">
<div class="ke-mtitle" id="ke-mtitle">CLICK TO ACTIVATE</div>
<div class="ke-msub" id="ke-msub">Engine loading...</div>
</div>
</div>
<div class="ke-eval-row">
<div class="ke-eval idle" id="ke-eval">—</div>
<div class="ke-movebox">
<div class="ke-movelbl">Best Move</div>
<div class="ke-move idle" id="ke-move">—</div>
</div>
</div>
<div class="ke-tabbar">
<div class="ke-tab act" data-tab="play">PLAY</div>
<div class="ke-tab" data-tab="tune">TUNE</div>
<div class="ke-tab" data-tab="stats">STATS</div>
</div>
<div class="ke-page act" id="ke-p-play">
<div class="ke-row"><span class="ke-rlbl">Auto-Play</span> <div class="ke-sw ${CFG.autoPlay?'on':''}" id="sw-auto"></div></div>
<div class="ke-row"><span class="ke-rlbl">Opening Book</span> <div class="ke-sw ${CFG.useBook?'on':''}" id="sw-book"></div></div>
<div class="ke-row"><span class="ke-rlbl">Show Threats</span> <div class="ke-sw ${CFG.showThreats?'on':''}" id="sw-thr"></div></div>
<div style="font-size:9.5px;color:#444;letter-spacing:0.09em;text-transform:uppercase;margin-bottom:7px">Time Control</div>
<div class="ke-tc-row">
<div class="ke-tc ${CFG.timeControl==='bullet'?'act':''}" data-tc="bullet">🔴 Bullet</div>
<div class="ke-tc ${CFG.timeControl==='blitz'?'act':''}" data-tc="blitz">🟡 Blitz</div>
<div class="ke-tc ${CFG.timeControl==='rapid'?'act':''}" data-tc="rapid">🟢 Rapid</div>
</div>
</div>
<div class="ke-page" id="ke-p-tune">
<div class="ke-slwrap">
<div class="ke-slhdr"><span>Engine Depth</span><span id="sv-dep">${CFG.depth}</span></div>
<input type="range" class="ke-sl" id="sl-dep" min="6" max="20" value="${CFG.depth}">
</div>
<div class="ke-slwrap">
<div class="ke-slhdr"><span>Best-Move Target %</span><span id="sv-cor">${Math.round(CFG.correlation*100)}</span></div>
<input type="range" class="ke-sl" id="sl-cor" min="40" max="98" value="${Math.round(CFG.correlation*100)}">
</div>
<div class="ke-slwrap">
<div class="ke-slhdr"><span>Suboptimal Rate %</span><span id="sv-sub">${Math.round(CFG.suboptimalRate*100)}</span></div>
<input type="range" class="ke-sl" id="sl-sub" min="5" max="60" value="${Math.round(CFG.suboptimalRate*100)}">
</div>
<div class="ke-row"><span class="ke-rlbl">Humanization</span><div class="ke-sw ${CFG.humanization?'on':''}" id="sw-hum"></div></div>
</div>
<div class="ke-page" id="ke-p-stats">
<div class="ke-stats">
<div class="ke-stat"><div class="ke-stat-l">Moves</div> <div class="ke-stat-v" id="st-mv">0</div></div>
<div class="ke-stat"><div class="ke-stat-l">Correlation</div><div class="ke-stat-v" id="st-co">—</div></div>
<div class="ke-stat"><div class="ke-stat-l">Best</div> <div class="ke-stat-v" id="st-be">0</div></div>
<div class="ke-stat"><div class="ke-stat-l">Eval</div> <div class="ke-stat-v" id="st-ev">—</div></div>
</div>
</div>
<div class="ke-footer">
<span><span class="ke-k">A</span> Auto</span>
<span><span class="ke-k">E</span> Toggle</span>
<span><span class="ke-k">X</span> Hide</span>
</div>
</div>
`;
document.body.appendChild(p);
UI.panel = p;
UI._drag(p);
UI._bind(p);
},
_drag: (el) => {
const hdr = el.querySelector('.ke-hdr');
let dragging = false, ox, oy, ol, ot;
const down = (cx, cy) => { dragging = true; ox = cx; oy = cy; ol = el.offsetLeft; ot = el.offsetTop; };
const move = (cx, cy) => { if (dragging) { el.style.left = (ol+cx-ox)+'px'; el.style.top = (ot+cy-oy)+'px'; } };
const up = () => { dragging = false; };
hdr.addEventListener('mousedown', e => { if (!e.target.classList.contains('ke-colbtn')) down(e.clientX, e.clientY); });
document.addEventListener('mousemove', e => move(e.clientX, e.clientY));
document.addEventListener('mouseup', up);
hdr.addEventListener('touchstart', e => { const t=e.touches[0]; if (!e.target.classList.contains('ke-colbtn')) down(t.clientX,t.clientY); }, {passive:true});
document.addEventListener('touchmove', e => { const t=e.touches[0]; move(t.clientX,t.clientY); }, {passive:true});
document.addEventListener('touchend', up);
},
_bind: (p) => {
// Collapse
const body = p.querySelector('#ke-body');
const colBtn = p.querySelector('#ke-col');
let col = false;
const doCol = e => {
e.preventDefault(); e.stopPropagation(); col = !col;
body.classList.toggle('col', col); colBtn.textContent = col ? '+' : '−';
};
colBtn.addEventListener('click', doCol);
colBtn.addEventListener('touchend', doCol, {passive:false});
// Master toggle
const masterBtn = p.querySelector('#ke-master');
const doMaster = e => {
e.stopPropagation();
CFG.active = !CFG.active;
masterBtn.classList.toggle('on', CFG.active);
p.querySelector('#ke-mtitle').textContent = CFG.active ? 'ENGINE ACTIVE' : 'CLICK TO ACTIVATE';
p.querySelector('#ke-msub').textContent = CFG.active ? 'Analysing every move' : 'Engine ready';
p.querySelector('#ke-dot').className = 'ke-dot ' + (CFG.active ? 'active' : (SF.ready ? 'ready' : 'loading'));
if (CFG.active) {
// Reset so gameLoop fires immediately on current position
State.lastFen = null;
Loop._lastAnalyzedFen = null;
SF._pendingFen = null;
requestAnimationFrame(Loop.tick);
} else {
SF.stop();
UI.clearArrows();
p.querySelector('#ke-eval').textContent = '—';
p.querySelector('#ke-eval').className = 'ke-eval idle';
p.querySelector('#ke-move').textContent = '—';
p.querySelector('#ke-move').className = 'ke-move idle';
}
};
masterBtn.addEventListener('click', doMaster);
masterBtn.addEventListener('touchend', e => { e.preventDefault(); doMaster(e); }, {passive:false});
// Tabs
p.querySelectorAll('.ke-tab').forEach(t => {
t.addEventListener('click', () => {
p.querySelectorAll('.ke-tab').forEach(x => x.classList.remove('act'));
p.querySelectorAll('.ke-page').forEach(x => x.classList.remove('act'));
t.classList.add('act');
p.querySelector('#ke-p-' + t.dataset.tab).classList.add('act');
});
});
// Switches
const sw = (id, key) => {
const el = p.querySelector(id);
if (!el) return;
el.addEventListener('click', function() { CFG[key] = !CFG[key]; this.classList.toggle('on', CFG[key]); });
};
sw('#sw-auto', 'autoPlay');
sw('#sw-book', 'useBook');
sw('#sw-thr', 'showThreats');
sw('#sw-hum', 'humanization');
// TC buttons
p.querySelectorAll('.ke-tc').forEach(b => {
b.addEventListener('click', () => {
CFG.timeControl = b.dataset.tc;
p.querySelectorAll('.ke-tc').forEach(x => x.classList.toggle('act', x.dataset.tc === b.dataset.tc));
// Apply the depth for this time control
const preset = CFG.timing[CFG.timeControl];
if (preset) {
CFG.depth = preset.depth;
const depSlider = p.querySelector('#sl-dep');
const depVal = p.querySelector('#sv-dep');
if (depSlider) {
depSlider.value = CFG.depth;
depSlider.style.setProperty('--v', ((CFG.depth - depSlider.min) / (depSlider.max - depSlider.min) * 100).toFixed(1) + '%');
}
if (depVal) depVal.textContent = CFG.depth;
}
});
});
// Sliders
const sl = (id, valId, setter) => {
const el = p.querySelector(id);
const vl = p.querySelector(valId);
if (!el) return;
const upd = () => {
const v = parseInt(el.value);
setter(v);
if (vl) vl.textContent = v;
el.style.setProperty('--v', ((v - el.min) / (el.max - el.min) * 100).toFixed(1) + '%');
};
el.addEventListener('input', upd);
upd();
};
sl('#sl-dep', '#sv-dep', v => { CFG.depth = v; });
sl('#sl-cor', '#sv-cor', v => { CFG.correlation = v / 100; });
sl('#sl-sub', '#sv-sub', v => { CFG.suboptimalRate = v / 100; });
// Keyboard
document.addEventListener('keydown', e => {
if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) return;
if (e.key === 'a') { CFG.autoPlay = !CFG.autoPlay; p.querySelector('#sw-auto').classList.toggle('on', CFG.autoPlay); }
if (e.key === 'e') doMaster(e);
if (e.key === 'x') {
UI._stealth = !UI._stealth;
p.classList.toggle('stealth', UI._stealth);
document.querySelectorAll('.ke-overlay').forEach(o => o.style.opacity = UI._stealth ? '0' : '1');
}
});
},
setStatus: (s) => {
const dot = UI.panel && UI.panel.querySelector('#ke-dot');
if (dot) dot.className = 'ke-dot ' + s;
const msub = UI.panel && UI.panel.querySelector('#ke-msub');
if (msub && !CFG.active) msub.textContent = s === 'ready' ? 'Engine ready' : (s === 'error' ? 'Load failed — reload page' : 'Loading engine...');
},
updateEval: (type, val) => {
if (!UI.panel) return;
try {
const el = UI.panel.querySelector('#ke-eval');
if (type === 'mate') {
el.textContent = 'M' + Math.abs(val);
el.className = 'ke-eval ' + (val > 0 ? 'pos' : 'neg');
} else {
el.textContent = (val > 0 ? '+' : '') + val.toFixed(2);
el.className = 'ke-eval ' + (val > 0.4 ? 'pos' : val < -0.4 ? 'neg' : 'neu');
}
const se = UI.panel.querySelector('#st-ev');
if (se) se.textContent = type === 'mate' ? 'M'+Math.abs(val) : (val > 0 ? '+' : '') + val.toFixed(1);
} catch(e) {}
},
updateMove: (move, isBest) => {
if (!UI.panel) return;
try {
const el = UI.panel.querySelector('#ke-move');
el.textContent = move || '—';
el.className = 'ke-move' + (move === '...' || move === '—' ? ' idle' : (isBest ? '' : ' sub'));
const sm = UI.panel.querySelector('#st-mv');
const sc = UI.panel.querySelector('#st-co');
const sb = UI.panel.querySelector('#st-be');
if (sm) sm.textContent = State.totalCount;
if (sb) sb.textContent = State.bestCount;
if (sc) sc.textContent = State.totalCount > 0 ? Math.round(State.bestCount / State.totalCount * 100) + '%' : '—';
} catch(e) {}
},
clearArrows: () => {
try { document.querySelectorAll('.ke-overlay').forEach(e => e.remove()); } catch(e) {}
},
drawArrows: (bestMove, pickedMove) => {
if (UI._stealth) return;
UI.clearArrows();
try {
const b = Board.el();
if (!b) return;
const rect = b.getBoundingClientRect();
if (!rect || !rect.width) return;
const overlay = document.createElement('div');
overlay.className = 'ke-overlay';
overlay.style.cssText = 'width:'+rect.width+'px;height:'+rect.height+'px;left:'+rect.left+'px;top:'+rect.top+'px;';
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.style.cssText = 'width:100%;height:100%;overflow:visible;';
overlay.appendChild(svg);
document.body.appendChild(overlay);
const sqSz = rect.width / 8;
const pos = (sq) => {
const flip = State.playerColor === 'b';
const f = sq.charCodeAt(0) - 97;
const r = parseInt(sq[1]) - 1;
return { x: (flip ? 7-f : f)*sqSz + sqSz/2, y: (flip ? r : 7-r)*sqSz + sqSz/2 };
};
const arrow = (mv, color, dashed) => {
if (!mv || mv.length < 4) return;
const p1 = pos(mv.slice(0,2));
const p2 = pos(mv.slice(2,4));
const dx = p2.x-p1.x, dy = p2.y-p1.y;
const len = Math.sqrt(dx*dx+dy*dy);
if (!len) return;
const ux = dx/len, uy = dy/len;
const sw = dashed ? sqSz*0.085 : sqSz*0.14;
const ex = p2.x - ux*sqSz*0.25, ey = p2.y - uy*sqSz*0.25;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', p1.x); line.setAttribute('y1', p1.y);
line.setAttribute('x2', ex); line.setAttribute('y2', ey);
line.setAttribute('stroke', color);
line.setAttribute('stroke-width', sw);
line.setAttribute('stroke-opacity', dashed ? '0.5' : '0.82');
line.setAttribute('stroke-linecap', 'round');
if (dashed) line.setAttribute('stroke-dasharray', '7,4');
svg.appendChild(line);
const tip = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
tip.setAttribute('cx', p2.x); tip.setAttribute('cy', p2.y);
tip.setAttribute('r', dashed ? sqSz*0.085 : sqSz*0.155);
tip.setAttribute('fill', color);
tip.setAttribute('opacity', dashed ? '0.5' : '0.82');
svg.appendChild(tip);
};
arrow(bestMove, '#4caf50', false);
if (pickedMove && pickedMove !== bestMove) arrow(pickedMove, '#ffc107', false);
if (CFG.showThreats && State.opponentMove) arrow(State.opponentMove, '#f44336', true);
} catch(e) { log('arrow error: ' + e.message, 'warn'); }
},
};
// ─── BOOT ────────────────────────────────────────────────────────────────────
(async () => {
UI.init();
if (_paused) { log('Inactive page — standby', 'warn'); return; }
// Wait for board (max ~12s)
let tries = 0;
while (!Board.el() && tries++ < 30) await sleep(400);
if (!Board.el()) { log('Board not found', 'error'); return; }
// Pre-load engine silently so it is hot when user activates
SF.init().then(ok => {
if (ok) { log('Engine ready — press E or click the button'); UI.setStatus('ready'); }
});
Loop.start();
// Redraw arrows if board shifts (resize, scroll, layout reflow)
const redraw = () => {
if (!CFG.active || _paused) return;
const overlays = document.querySelectorAll('.ke-overlay');
if (!overlays.length) return;
const b = Board.el();
if (!b) return;
const rect = b.getBoundingClientRect();
if (!rect || !rect.width) return;
overlays.forEach(o => {
o.style.left = rect.left + 'px';
o.style.top = rect.top + 'px';
o.style.width = rect.width + 'px';
o.style.height = rect.height + 'px';
});
};
window.addEventListener('resize', redraw, { passive: true });
window.addEventListener('scroll', redraw, { passive: true });
})();
})();