Greasy Fork is available in English.
Chess.com cheat engine — K-EXPERT edition (ELO + Stream-Hide + PlayStyle)
// ==UserScript==
// @name Chess.com Cheat Engine
// @namespace http://tampermonkey.net/
// @version 16.0
// @description Chess.com cheat engine — K-EXPERT edition (ELO + Stream-Hide + PlayStyle)
// @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',
eloMode: false,
targetElo: 1500,
eloTc: 'blitz',
style: 'balanced', // ← NEW: playing style
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) => {
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');
// ─── ELO STRENGTH ENGINE ─────────────────────────────────────────────────────
const EloStrength = {
toUciElo: (elo) => Math.max(1320, Math.min(3190, elo)),
toSkill: (elo) => {
if (elo < 800) return 0;
if (elo < 900) return 1;
if (elo < 1000) return 2;
if (elo < 1100) return 3;
if (elo < 1200) return 4;
if (elo < 1300) return 5;
if (elo < 1400) return 6;
if (elo < 1500) return 8;
if (elo < 1600) return 10;
if (elo < 1700) return 11;
if (elo < 1800) return 12;
if (elo < 1900) return 13;
if (elo < 2000) return 14;
if (elo < 2100) return 15;
if (elo < 2200) return 16;
if (elo < 2400) return 17;
if (elo < 2600) return 18;
if (elo < 2800) return 19;
return 20;
},
toDepth: (elo) => {
if (elo < 700) return 1;
if (elo < 900) return 2;
if (elo < 1100) return 3;
if (elo < 1300) return 5;
if (elo < 1500) return 7;
if (elo < 1700) return 9;
if (elo < 1900) return 11;
if (elo < 2100) return 13;
if (elo < 2300) return 15;
if (elo < 2500) return 17;
if (elo < 2700) return 19;
return 20;
},
toSuboptimalRate: (elo) => {
if (elo < 600) return 0.85;
if (elo < 800) return 0.70;
if (elo < 900) return 0.58;
if (elo < 1000) return 0.50;
if (elo < 1100) return 0.43;
if (elo < 1200) return 0.37;
if (elo < 1300) return 0.32;
if (elo < 1400) return 0.27;
if (elo < 1500) return 0.23;
if (elo < 1600) return 0.19;
if (elo < 1700) return 0.15;
if (elo < 1800) return 0.12;
if (elo < 1900) return 0.09;
if (elo < 2000) return 0.07;
if (elo < 2100) return 0.055;
if (elo < 2200) return 0.04;
if (elo < 2400) return 0.025;
if (elo < 2600) return 0.015;
return 0.008;
},
toMaxLoss: (elo) => {
if (elo < 800) return 600;
if (elo < 900) return 450;
if (elo < 1000) return 350;
if (elo < 1100) return 280;
if (elo < 1200) return 220;
if (elo < 1300) return 170;
if (elo < 1400) return 130;
if (elo < 1500) return 100;
if (elo < 1600) return 75;
if (elo < 1700) return 55;
if (elo < 1800) return 40;
if (elo < 2000) return 28;
if (elo < 2200) return 18;
if (elo < 2400) return 10;
return 6;
},
toTiming: (elo, tc) => {
const base = {
bullet: [
[0, 500, 100, 400],
[500, 1000, 200, 600],
[1000, 1500, 300, 700],
[1500, 2000, 250, 650],
[2000, 9999, 200, 550],
],
blitz: [
[0, 500, 500, 2000],
[500, 1000, 800, 3000],
[1000, 1500, 1000, 4000],
[1500, 2000, 1500, 5000],
[2000, 2500, 2000, 6000],
[2500, 9999, 2500, 7000],
],
rapid: [
[0, 500, 1000, 5000],
[500, 1000, 2000, 8000],
[1000, 1500, 3000, 10000],
[1500, 2000, 4000, 13000],
[2000, 2500, 5000, 16000],
[2500, 9999, 6000, 20000],
],
};
const rows = base[tc] || base.blitz;
for (const [lo, hi, mn, mx] of rows) {
if (elo >= lo && elo < hi) return { min: mn, max: mx };
}
return { min: 2000, max: 8000 };
},
label: (elo) => {
if (elo < 800) return 'Beginner';
if (elo < 1000) return 'Novice';
if (elo < 1200) return 'Class D';
if (elo < 1400) return 'Class C';
if (elo < 1600) return 'Class B';
if (elo < 1800) return 'Class A';
if (elo < 2000) return 'Candidate';
if (elo < 2200) return 'Expert';
if (elo < 2400) return 'NM / FM';
if (elo < 2500) return 'IM';
if (elo < 2700) return 'GM';
return 'Super-GM';
},
apply: (elo) => {
if (!SF.ready || !SF.worker) return;
const skill = EloStrength.toSkill(elo);
const uciElo = EloStrength.toUciElo(elo);
SF.worker.postMessage('setoption name UCI_LimitStrength value true');
SF.worker.postMessage('setoption name UCI_Elo value ' + uciElo);
SF.worker.postMessage('setoption name Skill Level value ' + skill);
log('ELO ' + elo + ' → Skill ' + skill + ', UCI_Elo ' + uciElo + ', Depth ' + EloStrength.toDepth(elo));
},
reset: () => {
if (!SF.ready || !SF.worker) return;
SF.worker.postMessage('setoption name UCI_LimitStrength value false');
SF.worker.postMessage('setoption name Skill Level value 20');
log('ELO mode OFF — full strength');
},
};
// ─── PLAY STYLE ENGINE ───────────────────────────────────────────────────────
//
// Each style physically changes WHICH move is selected from the MultiPV
// candidate list — not just a label. Logic:
//
// • boardMap() — parse FEN into a square→piece lookup
// • classify() — tag each candidate move (capture? sacrifice? equal trade?)
// • pickMove() — per-style weighted selection from classified candidates
//
// Styles:
// balanced — default randomised humanization (no override)
// aggressive — prefers captures of high-value pieces; willing to sac slightly
// defensive — avoids captures; never sacrifices; plays solid non-tactical moves
// brilliant — actively seeks sacrifices (give up higher-value piece);
// if no sac available, tries the most forcing capture
// endgame — trades pieces toward a favourable endgame; in endgame phase
// always plays the single best move (no randomisation)
// positional — avoids all captures when quiet alternatives exist; prefers
// long-term manoeuvring over tactics
//
const PlayStyle = {
// ── Meta-info used by UI ──────────────────────────────────────────────
LABELS: {
balanced: { icon: '⚖️', label: 'Balanced', color: '#9e9e9e' },
aggressive: { icon: '🔥', label: 'Aggressive', color: '#f44336' },
defensive: { icon: '🛡️', label: 'Defensive', color: '#2196f3' },
brilliant: { icon: '✨', label: 'Brilliant', color: '#9c27b0' },
endgame: { icon: '👑', label: 'Endgame', color: '#ffc107' },
positional: { icon: '♟️', label: 'Positional', color: '#00bcd4' },
},
PIECE_VAL: { p: 1, n: 3, b: 3, r: 5, q: 9, k: 100 },
// Parse FEN board section into { square: { piece, color, val } }
boardMap: (fen) => {
const map = {};
const rows = (fen || '').split(' ')[0].split('/');
for (let r = 0; r < 8; r++) {
let file = 0;
for (const ch of (rows[r] || '')) {
if (/\d/.test(ch)) { file += parseInt(ch); }
else {
const sq = String.fromCharCode(97 + file) + (8 - r);
const lower = ch.toLowerCase();
map[sq] = { piece: lower, color: ch === ch.toUpperCase() ? 'w' : 'b', val: PlayStyle.PIECE_VAL[lower] || 0 };
file++;
}
}
}
return map;
},
// Classify a move given the board map and whose turn it is
classify: (move, map, myColor) => {
const from = move.slice(0, 2);
const to = move.slice(2, 4);
const mover = map[from];
const target = map[to];
const isCapture = !!(target && target.color !== myColor);
const isSacrifice = isCapture && !!mover && !!target && mover.val > target.val;
const isEqualTrade = isCapture && !!mover && !!target && mover.val === target.val;
return {
isCapture,
isSacrifice,
isEqualTrade,
moverVal: mover ? mover.val : 0,
targetVal: target ? target.val : 0,
};
},
// Main entry point — returns { move, best } or null (null = use default logic)
pickMove: (bestMove, fen) => {
const style = CFG.style;
if (!style || style === 'balanced') return null;
const best = State.candidates[1];
if (!best) return null;
const sideToMove = (fen || '').split(' ')[1] || 'w';
const map = PlayStyle.boardMap(fen);
// Build annotated candidate list
const allC = [];
for (let i = 1; i <= CFG.multiPV; i++) {
const c = State.candidates[i];
if (!c || !c.move) continue;
const cl = PlayStyle.classify(c.move, map, sideToMove);
// centipawn loss vs best (always ≥ 0)
const loss = (c.eval.type === 'cp' && best.eval.type === 'cp')
? Math.max(0, (best.eval.val - c.eval.val) * 100)
: 0;
allC.push({ ...c, rank: i, loss, ...cl });
}
if (!allC.length) return null;
switch (style) {
// ── AGGRESSIVE ─────────────────────────────────────────────────
// Heavily weight safe captures of high-value pieces.
// Accepts equal-value trades freely. Will take a small loss (≤80cp)
// for an exciting capture. Sacrifices are considered if loss ≤ 80cp.
case 'aggressive': {
const pool = allC.filter(c => c.loss <= 130);
if (!pool.length) return null;
const w = pool.map(c => {
if (c.isSacrifice && c.loss <= 80) return 2.5;
if (c.isCapture && !c.isSacrifice) return 4 + c.targetVal * 0.8;
return 1;
});
const picked = PlayStyle._wPick(pool, w);
log('AGGRESSIVE pick: ' + (picked && picked.move));
return picked;
}
// ── DEFENSIVE ──────────────────────────────────────────────────
// Strongly prefers quiet non-capturing moves.
// Never sacrifices. Accepts captures only when winning clear material.
// Very consistent — essentially no randomisation beyond style weighting.
case 'defensive': {
const pool = allC.filter(c => c.loss <= 45 && !c.isSacrifice);
if (!pool.length) return { move: bestMove, best: true };
const w = pool.map(c => {
if (!c.isCapture) return 4; // strongly prefer quiet
if (c.targetVal > c.moverVal) return 2; // winning capture — ok
if (c.isEqualTrade) return 0.8; // equal trade — reluctant
return 0.3; // anything else — avoid
});
const picked = PlayStyle._wPick(pool, w);
log('DEFENSIVE pick: ' + (picked && picked.move));
return picked;
}
// ── BRILLIANT ──────────────────────────────────────────────────
// Actively hunts for sacrifices every move.
// Prefers giving up the highest-value piece possible within 200cp of best.
// If no sacrifice is available, goes for the most forcing capture.
// Falls back to best move only when position is completely forced/quiet.
case 'brilliant': {
const sacrifices = allC.filter(c => c.isSacrifice && c.loss <= 200);
if (sacrifices.length) {
// Prefer bigger piece sacrificed, then smallest eval loss
sacrifices.sort((a, b) =>
b.moverVal !== a.moverVal ? b.moverVal - a.moverVal : a.loss - b.loss
);
const s = sacrifices[0];
log('BRILLIANT sacrifice: ' + s.move + ' (gives up ' + s.moverVal + ' for ' + s.targetVal + ')');
return { move: s.move, best: s.rank === 1 };
}
// No sac available — play the most valuable capture (forcing)
const captures = allC.filter(c => c.isCapture && c.loss <= 100);
if (captures.length) {
captures.sort((a, b) => b.targetVal - a.targetVal || a.loss - b.loss);
log('BRILLIANT best capture: ' + captures[0].move);
return { move: captures[0].move, best: captures[0].rank === 1 };
}
// No captures at all — play best move (positional position)
return { move: bestMove, best: true };
}
// ── ENDGAME SPECIALIST ─────────────────────────────────────────
// In opening/middlegame: steers toward piece trades to reach a
// favourable endgame. Prefers equal trades, then winning captures.
// In endgame phase: bypasses ALL randomisation and always plays
// the single engine-best move.
case 'endgame': {
const phase = Human.phase(fen);
if (phase === 'end') {
log('ENDGAME specialist — endgame phase, playing best');
return { move: bestMove, best: true };
}
// Middlegame: prefer equal trades (simplify)
const equalTrades = allC.filter(c => c.isEqualTrade && c.loss <= 70);
if (equalTrades.length) {
equalTrades.sort((a, b) => b.targetVal - a.targetVal);
log('ENDGAME trade: ' + equalTrades[0].move + ' (trading ' + equalTrades[0].targetVal + ')');
return { move: equalTrades[0].move, best: equalTrades[0].rank === 1 };
}
// Accept winning captures (simplification)
const winCaps = allC.filter(c => c.isCapture && !c.isSacrifice && c.loss <= 55);
if (winCaps.length) {
winCaps.sort((a, b) => b.targetVal - a.targetVal);
log('ENDGAME winning cap: ' + winCaps[0].move);
return { move: winCaps[0].move, best: winCaps[0].rank === 1 };
}
return null; // let normal logic handle it
}
// ── POSITIONAL ─────────────────────────────────────────────────
// Prefers quiet, strategic non-capturing moves (long-term thinking).
// Avoids all tactical complications unless forced.
// Weights quiet moves by engine rank (best quiet move gets most weight).
case 'positional': {
const quietPool = allC.filter(c => !c.isCapture && c.loss <= 80);
if (quietPool.length) {
// Weight by inverse rank: rank 1 gets highest weight
const w = quietPool.map(c => 1 / c.rank);
const picked = PlayStyle._wPick(quietPool, w);
log('POSITIONAL quiet pick: ' + (picked && picked.move));
return picked;
}
// All moves are captures — play best (forced recapture etc.)
return { move: bestMove, best: true };
}
}
return null;
},
// Weighted random selection from a candidate array
_wPick: (candidates, weights) => {
if (!candidates.length) return null;
const total = weights.reduce((s, w) => s + w, 0);
if (total <= 0) return { move: candidates[0].move, best: candidates[0].rank === 1 };
let r = Math.random() * total;
for (let i = 0; i < candidates.length; i++) {
r -= weights[i];
if (r <= 0) return { move: candidates[i].move, best: candidates[i].rank === 1 };
}
return { move: candidates[0].move, best: candidates[0].rank === 1 };
},
// Short description of the active style shown in the UI
describe: (style) => {
const d = {
balanced: 'Normal humanised engine play',
aggressive: 'Hunts captures & high-value pieces',
defensive: 'Avoids captures, plays solid & safe',
brilliant: 'Sacrifices material every chance it gets',
endgame: 'Trades into endgame, then plays perfectly',
positional: 'Quiet strategic moves, avoids tactics',
};
return d[style] || '';
},
};
// ─── STREAM HIDE — FLOATING MINI HUD ────────────────────────────────────────
const StreamHide = {
active: false,
_el: null,
_raf: null,
toggle: () => {
StreamHide.active ? StreamHide.stop() : StreamHide.start();
},
start: () => {
StreamHide._buildHUD();
StreamHide.active = true;
if (UI.panel) UI.panel.classList.add('ke-sh-hidden');
document.querySelectorAll('.ke-overlay').forEach(o => o.style.opacity = '0');
StreamHide._loop();
UI._refreshStreamBtn();
log('Stream-Hide ON — drag the mini HUD outside your capture region');
},
stop: () => {
StreamHide.active = false;
if (StreamHide._raf) { cancelAnimationFrame(StreamHide._raf); StreamHide._raf = null; }
if (StreamHide._el) { StreamHide._el.remove(); StreamHide._el = null; }
if (UI.panel) UI.panel.classList.remove('ke-sh-hidden');
document.querySelectorAll('.ke-overlay').forEach(o => o.style.opacity = '');
UI._refreshStreamBtn();
log('Stream-Hide OFF');
},
_loop: () => {
if (!StreamHide.active || !StreamHide._el) return;
StreamHide._update();
StreamHide._raf = requestAnimationFrame(StreamHide._loop);
},
_buildHUD: () => {
if (StreamHide._el) { StreamHide._el.remove(); StreamHide._el = null; }
const el = document.createElement('div');
el.id = 'ke-float-hud';
Object.assign(el.style, {
position:'fixed', top:'16px', right:'16px',
width:'320px', background:'#0d0d10',
border:'1px solid rgba(255,255,255,0.10)',
borderRadius:'10px',
fontFamily:'Inter,system-ui,sans-serif',
boxShadow:'0 16px 48px rgba(0,0,0,0.8)',
zIndex:'2147483647',
userSelect:'none', webkitUserSelect:'none',
touchAction:'none', overflow:'hidden', cursor:'grab',
});
el.innerHTML = [
'<div id="ke-fhud-bar" style="padding:8px 10px 7px;background:rgba(255,255,255,0.03);border-bottom:1px solid rgba(255,255,255,0.06);display:flex;align-items:center;gap:8px;">',
'<span style="font-size:10px;font-weight:700;letter-spacing:0.09em;background:linear-gradient(120deg,#69f0ae,#4caf50);-webkit-background-clip:text;-webkit-text-fill-color:transparent;flex:1">K-EXPERT</span>',
'<span id="ke-fhud-mode-lbl" style="font-size:9px;font-weight:600;color:#555;letter-spacing:0.06em">STANDBY</span>',
'<span id="ke-fhud-dot" style="width:6px;height:6px;border-radius:50%;background:#444;flex-shrink:0;margin-left:4px"></span>',
'<span id="ke-fhud-close" style="margin-left:4px;width:18px;height:18px;border-radius:4px;background:rgba(255,255,255,0.06);display:flex;align-items:center;justify-content:center;font-size:11px;cursor:pointer;flex-shrink:0;color:#666;-webkit-text-fill-color:#666;line-height:18px;text-align:center">✕</span>',
'</div>',
'<div style="padding:10px 10px 7px;display:grid;grid-template-columns:1fr 1fr;gap:7px;">',
'<div style="background:#111115;border:1px solid rgba(255,255,255,0.06);border-radius:7px;padding:8px 10px;">',
'<div style="font-size:8px;color:#444;letter-spacing:0.09em;margin-bottom:4px">EVAL</div>',
'<div id="ke-fhud-eval" style="font-family:JetBrains Mono,Menlo,monospace;font-size:30px;font-weight:700;color:#444;line-height:1">—</div>',
'</div>',
'<div style="background:#111115;border:1px solid rgba(255,255,255,0.06);border-radius:7px;padding:8px 10px;">',
'<div style="font-size:8px;color:#444;letter-spacing:0.09em;margin-bottom:4px">BEST MOVE</div>',
'<div id="ke-fhud-move" style="font-family:JetBrains Mono,Menlo,monospace;font-size:20px;font-weight:700;color:#444;line-height:1;padding:4px 0;border-radius:5px">—</div>',
'</div>',
'</div>',
'<div id="ke-fhud-threat-row" style="display:none;margin:0 10px 7px;background:rgba(244,67,54,0.07);border:1px solid rgba(244,67,54,0.18);border-radius:6px;padding:6px 10px;font-family:JetBrains Mono,Menlo,monospace;font-size:12px;font-weight:700;color:#f44336">',
'<span style="font-family:Inter,system-ui,sans-serif;font-size:8px;opacity:0.7;letter-spacing:0.09em;font-weight:600;-webkit-text-fill-color:#f44336">OPP THREAT </span>',
'<span id="ke-fhud-threat"></span>',
'</div>',
'<div id="ke-fhud-elo-strip" style="margin:0 10px 7px;background:rgba(76,175,80,0.07);border:1px solid rgba(76,175,80,0.12);border-radius:6px;padding:6px 10px;font-size:9px;font-weight:600;color:#69f0ae;letter-spacing:0.04em">ENGINE STANDBY</div>',
// Style strip — shown in HUD
'<div id="ke-fhud-style-strip" style="margin:0 10px 8px;border-radius:6px;padding:5px 10px;font-size:9px;font-weight:700;letter-spacing:0.05em;border:1px solid rgba(255,255,255,0.06);color:#555;background:#111115">⚖️ BALANCED</div>',
'<div style="padding:0 10px 9px;display:flex;gap:5px;">',
...[['MOVES','ke-fhud-v1','0'],['BEST','ke-fhud-v2','0'],['CORR','ke-fhud-v3','—'],['TC','ke-fhud-v4','—']].map(([l,id,d]) =>
`<div style="flex:1;background:#111115;border:1px solid rgba(255,255,255,0.05);border-radius:5px;padding:6px 8px;"><div style="font-size:7px;color:#444;letter-spacing:0.08em;margin-bottom:2px">${l}</div><div id="${id}" style="font-family:JetBrains Mono,Menlo,monospace;font-size:15px;font-weight:700;color:#ccc">${d}</div></div>`
),
'</div>',
'<div style="padding:5px 10px 6px;border-top:1px solid rgba(255,255,255,0.04);font-size:8px;color:#2a2a35;text-align:center">Drag to reposition · ✕ to close</div>',
].join('');
document.body.appendChild(el);
StreamHide._el = el;
// ── Drag ─────────────────────────────────────────────────────────
let drag=false, ox=0, oy=0, sl=0, st=0;
const ds = (cx,cy) => {
drag=true; ox=cx; oy=cy;
const r=el.getBoundingClientRect(); sl=r.left; st=r.top;
el.style.cursor='grabbing'; el.style.right='auto';
el.style.left=sl+'px'; el.style.top=st+'px';
};
const dm = (cx,cy) => { if(!drag)return; el.style.left=(sl+cx-ox)+'px'; el.style.top=(st+cy-oy)+'px'; };
const de = () => { drag=false; el.style.cursor='grab'; };
el.addEventListener('mousedown', e => { if(e.target.id==='ke-fhud-close')return; ds(e.clientX,e.clientY); });
document.addEventListener('mousemove', e => dm(e.clientX,e.clientY));
document.addEventListener('mouseup', de);
el.addEventListener('touchstart', e => {
if(e.target.id==='ke-fhud-close')return;
const t=e.touches[0]; ds(t.clientX,t.clientY);
}, {passive:true});
document.addEventListener('touchmove', e => {
if(!drag)return; e.preventDefault();
const t=e.touches[0]; dm(t.clientX,t.clientY);
}, {passive:false});
document.addEventListener('touchend', de);
el.querySelector('#ke-fhud-close').addEventListener('click', e => { e.stopPropagation(); StreamHide.stop(); });
},
_update: () => {
const el = StreamHide._el;
if (!el) return;
// Eval
const ev = State.currentEval;
const evEl = el.querySelector('#ke-fhud-eval');
if (evEl) {
if (!ev) { evEl.textContent='—'; evEl.style.color='#444'; }
else if (ev.type==='mate') { evEl.textContent='M'+Math.abs(ev.val); evEl.style.color=ev.val>0?'#4caf50':'#f44336'; }
else { evEl.textContent=(ev.val>0?'+':'')+ev.val.toFixed(2); evEl.style.color=ev.val>0.4?'#4caf50':ev.val<-0.4?'#f44336':'#e0e0e0'; }
}
// Move
const srcMv = UI.panel && UI.panel.querySelector('#ke-move');
const mvEl = el.querySelector('#ke-fhud-move');
if (mvEl && srcMv) {
const txt=srcMv.textContent||'—';
const best=!srcMv.classList.contains('sub');
const idle=txt==='—'||txt==='...';
mvEl.textContent=txt;
mvEl.style.color=idle?'#444':best?'#4caf50':'#ffc107';
mvEl.style.background=idle?'transparent':best?'rgba(76,175,80,0.12)':'rgba(255,193,7,0.12)';
mvEl.style.padding=idle?'4px 0':'4px 8px';
}
// Threat
const tRow=el.querySelector('#ke-fhud-threat-row');
const tEl=el.querySelector('#ke-fhud-threat');
if (tRow&&tEl) {
const show=CFG.showThreats&&State.opponentMove&&State.opponentMove!=='—';
tRow.style.display=show?'block':'none';
if(show) tEl.textContent=State.opponentMove;
}
// ELO / mode strip
const strip=el.querySelector('#ke-fhud-elo-strip');
const modeLbl=el.querySelector('#ke-fhud-mode-lbl');
if (strip) {
let txt,col,bg,bd;
if (!CFG.active) { txt='ENGINE STANDBY'; col='#555'; bg='rgba(255,255,255,0.03)'; bd='rgba(255,255,255,0.06)'; }
else if (CFG.eloMode) { txt='ELO '+CFG.targetElo+' '+EloStrength.label(CFG.targetElo)+' Sk'+EloStrength.toSkill(CFG.targetElo)+' D'+EloStrength.toDepth(CFG.targetElo); col='#ce93d8'; bg='rgba(156,39,176,0.10)'; bd='rgba(156,39,176,0.22)'; }
else { txt='FULL STRENGTH D'+CFG.depth+' '+(CFG.useBook?'Book ON':'Book OFF'); col='#69f0ae'; bg='rgba(76,175,80,0.07)'; bd='rgba(76,175,80,0.12)'; }
strip.textContent=txt; strip.style.color=col; strip.style.background=bg; strip.style.borderColor=bd;
if (modeLbl) { modeLbl.textContent=CFG.active?(CFG.eloMode?'ELO '+CFG.targetElo:'ACTIVE'):'STANDBY'; modeLbl.style.color=col; }
}
// Style strip (NEW)
const styleStrip = el.querySelector('#ke-fhud-style-strip');
if (styleStrip) {
const sl = PlayStyle.LABELS[CFG.style] || PlayStyle.LABELS.balanced;
styleStrip.textContent = sl.icon + ' ' + sl.label.toUpperCase();
styleStrip.style.color = CFG.active ? sl.color : '#555';
styleStrip.style.background = CFG.active ? 'rgba(0,0,0,0.3)' : '#111115';
styleStrip.style.borderColor = CFG.active ? sl.color + '44' : 'rgba(255,255,255,0.06)';
}
// Dot
const dot=el.querySelector('#ke-fhud-dot');
if (dot) {
const c=!CFG.active?'#444':CFG.eloMode?'#9c27b0':'#4caf50';
dot.style.background=c; dot.style.boxShadow=CFG.active?'0 0 6px '+c:'none';
}
// Stats
const corr=State.totalCount>0?Math.round(State.bestCount/State.totalCount*100)+'%':'—';
[String(State.totalCount),String(State.bestCount),corr,CFG.timeControl.toUpperCase()].forEach((v,i)=>{
const e=el.querySelector('#ke-fhud-v'+(i+1)); if(e) e.textContent=v;
});
},
};
// ─── 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);
if (CFG.eloMode && CFG.targetElo < 1200) 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 (CFG.eloMode) EloStrength.apply(CFG.targetElo);
if (SF._resolveInit) { SF._resolveInit(true); SF._resolveInit = null; }
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);
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);
if (CFG.eloMode) {
const skill = EloStrength.toSkill(CFG.targetElo);
const uciElo = Math.max(1320, Math.min(3190, CFG.targetElo));
SF.worker.postMessage('setoption name UCI_LimitStrength value true');
SF.worker.postMessage('setoption name UCI_Elo value ' + uciElo);
SF.worker.postMessage('setoption name Skill Level value ' + skill);
SF.worker.postMessage('go depth ' + EloStrength.toDepth(CFG.targetElo));
} else {
SF.worker.postMessage('setoption name UCI_LimitStrength value false');
SF.worker.postMessage('setoption name Skill Level value 20');
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');
} 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 };
// ── STYLE OVERRIDE (runs before normal humanization) ───────────────
// PlayStyle.pickMove() returns null when style='balanced' or has no
// style-appropriate candidate — in that case we fall through.
if (CFG.style && CFG.style !== 'balanced') {
const stylePick = PlayStyle.pickMove(bestMove, State.lastFen || '');
if (stylePick) return stylePick;
}
// ── DEFAULT HUMANIZATION (unchanged) ──────────────────────────────
const best = State.candidates[1];
if (!best) return { move: bestMove, best: true };
if (State.perfectStreak >= 4) { const s = Human._subopt(); if (s) return s; }
if (State.sloppyStreak >= 3) return { move: bestMove, best: true };
let rate;
if (CFG.eloMode) {
rate = EloStrength.toSuboptimalRate(CFG.targetElo);
} else {
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 defaultMax = ({ open: 50, mid: 100, end: 60 })[Human.phase(State.lastFen)] || 80;
const maxLoss = CFG.eloMode ? EloStrength.toMaxLoss(CFG.targetElo) : defaultMax;
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: () => {
if (CFG.eloMode) {
const t = EloStrength.toTiming(CFG.targetElo, CFG.eloTc);
return humanMs(t.min, t.max);
}
const tc = CFG.timing[CFG.timeControl] || CFG.timing.blitz;
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);
if (!StreamHide.active) 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);
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();
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;
if (!StreamHide.active) 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 {
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 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; }
.ke-panel.ke-sh-hidden { opacity: 0 !important; pointer-events: none; }
.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.elo { background: #9c27b0; box-shadow: 0 0 7px #9c27b0; animation: ke-pulse 1.8s ease-in-out infinite; }
.ke-dot.stream { background: #2196f3; box-shadow: 0 0 7px #2196f3; animation: ke-pulse 0.9s 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; }
.ke-body { overflow: hidden; }
.ke-body.col { max-height: 0 !important; overflow: hidden; }
.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); }
.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; }
.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); }
.ke-page { display: none; padding: 0 12px 12px; }
.ke-page.act { display: block; }
.ke-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 9px; }
.ke-rlbl { font-size: 11px; color: #888; }
.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); }
.ke-sw.elo-sw.on { background: #9c27b0; border-color: #9c27b0; }
.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; }
.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); }
/* ── Stream-Hide button ───────────────────────────────────────── */
.ke-stream-btn {
margin: 0 12px 10px; padding: 9px 13px; border-radius: 8px;
border: 1px solid rgba(255,255,255,0.07); background: #141418;
cursor: pointer; display: flex; align-items: center; gap: 9px;
transition: border-color 0.2s, box-shadow 0.2s; position: relative; overflow: hidden;
}
.ke-stream-btn-bg { position: absolute; inset: 0; background: linear-gradient(120deg,#0d47a1,#1565c0); opacity: 0; transition: opacity 0.2s; pointer-events: none; }
.ke-stream-btn.on .ke-stream-btn-bg { opacity: 1; }
.ke-stream-btn.on { border-color: #2196f3; box-shadow: 0 0 16px rgba(33,150,243,0.3); }
.ke-stream-btn:hover { border-color: rgba(255,255,255,0.14); }
.ke-stream-icon { width: 28px; height: 28px; 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-stream-btn.on .ke-stream-icon { background: rgba(255,255,255,0.14); }
.ke-stream-txt { position: relative; z-index: 1; }
.ke-stream-title { font-size: 11px; font-weight: 700; letter-spacing: 0.07em; color: #555; transition: color 0.2s; }
.ke-stream-btn.on .ke-stream-title { color: #fff; }
.ke-stream-sub { font-size: 9.5px; color: #444; margin-top: 1px; transition: color 0.2s; }
.ke-stream-btn.on .ke-stream-sub { color: rgba(255,255,255,0.6); }
.ke-stream-badge {
margin-left: auto; position: relative; z-index: 1;
font-size: 8px; font-weight: 700; letter-spacing: 0.06em;
background: rgba(33,150,243,0.2); color: #64b5f6;
padding: 2px 6px; border-radius: 3px; border: 1px solid rgba(33,150,243,0.3);
}
.ke-stream-btn.on .ke-stream-badge { background: rgba(255,255,255,0.15); color: #fff; border-color: rgba(255,255,255,0.2); }
/* ── ELO block ───────────────────────────────────────────────── */
.ke-elo-block { background: #0f0f14; border: 1px solid rgba(255,255,255,0.06); border-radius: 8px; padding: 10px 11px 11px; margin-bottom: 10px; transition: border-color 0.2s; }
.ke-elo-block.elo-on { border-color: rgba(156,39,176,0.5); box-shadow: 0 0 14px rgba(156,39,176,0.15); }
.ke-elo-hdr { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.ke-elo-title { font-size: 10.5px; font-weight: 700; letter-spacing: 0.08em; color: #777; }
.ke-elo-block.elo-on .ke-elo-title { color: #ce93d8; }
.ke-elo-input-row { display: flex; align-items: center; gap: 7px; margin-bottom: 8px; }
.ke-elo-input { flex: 1; background: #1a1a22; border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; color: #e0e0e0; font-family: 'JetBrains Mono',monospace; font-size: 18px; font-weight: 600; text-align: center; padding: 6px 8px; outline: none; transition: border-color 0.15s; -moz-appearance: textfield; }
.ke-elo-input::-webkit-inner-spin-button,.ke-elo-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.ke-elo-input:focus { border-color: #9c27b0; }
.ke-elo-block.elo-on .ke-elo-input { border-color: rgba(156,39,176,0.4); }
.ke-elo-badge { font-size: 9.5px; font-weight: 700; letter-spacing: 0.06em; background: rgba(156,39,176,0.15); color: #ce93d8; padding: 3px 7px; border-radius: 4px; white-space: nowrap; min-width: 70px; text-align: center; }
.ke-elo-bar-wrap { position: relative; height: 4px; border-radius: 2px; background: #1e1e24; overflow: hidden; }
.ke-elo-bar { height: 100%; border-radius: 2px; background: linear-gradient(90deg,#4caf50,#ffc107,#f44336,#9c27b0); background-size: 400% 100%; transition: width 0.3s; }
.ke-elo-ticks { display: flex; justify-content: space-between; margin-top: 4px; font-size: 8px; color: #333; }
.ke-elo-presets { display: flex; gap: 4px; margin-top: 8px; flex-wrap: wrap; }
.ke-elo-preset { flex: 1; min-width: 0; padding: 4px 0; font-size: 9px; font-weight: 600; border-radius: 4px; cursor: pointer; text-align: center; border: 1px solid rgba(255,255,255,0.06); color: #444; background: #141418; transition: 0.15s; white-space: nowrap; overflow: hidden; }
.ke-elo-preset:hover { color: #ccc; border-color: rgba(255,255,255,0.15); }
.ke-elo-preset.act { background: rgba(156,39,176,0.25); color: #ce93d8; border-color: rgba(156,39,176,0.5); }
/* ── Playing Style selector ──────────────────────────────────── */
.ke-style-section-lbl {
font-size: 9.5px; color: #444; letter-spacing: 0.09em; text-transform: uppercase;
margin-bottom: 6px; margin-top: 2px;
}
.ke-style-grid {
display: grid; grid-template-columns: 1fr 1fr 1fr;
gap: 4px; margin-bottom: 10px;
}
.ke-style-btn {
padding: 7px 4px 6px; border-radius: 6px; cursor: pointer; text-align: center;
border: 1px solid rgba(255,255,255,0.07); background: #141418;
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
display: flex; flex-direction: column; align-items: center; gap: 3px;
}
.ke-style-btn:hover { border-color: rgba(255,255,255,0.18); background: #1a1a20; }
.ke-style-btn .ke-si { font-size: 14px; line-height: 1; }
.ke-style-btn .ke-sl { font-size: 8px; font-weight: 700; letter-spacing: 0.06em; color: #444; transition: color 0.15s; }
/* Active states per style */
.ke-style-btn.act[data-style=balanced] { background: rgba(158,158,158,0.12); border-color: rgba(158,158,158,0.45); box-shadow: 0 0 10px rgba(158,158,158,0.12); }
.ke-style-btn.act[data-style=balanced] .ke-sl { color: #9e9e9e; }
.ke-style-btn.act[data-style=aggressive] { background: rgba(244,67,54,0.12); border-color: rgba(244,67,54,0.50); box-shadow: 0 0 10px rgba(244,67,54,0.15); }
.ke-style-btn.act[data-style=aggressive] .ke-sl { color: #f44336; }
.ke-style-btn.act[data-style=defensive] { background: rgba(33,150,243,0.12); border-color: rgba(33,150,243,0.50); box-shadow: 0 0 10px rgba(33,150,243,0.15); }
.ke-style-btn.act[data-style=defensive] .ke-sl { color: #64b5f6; }
.ke-style-btn.act[data-style=brilliant] { background: rgba(156,39,176,0.14); border-color: rgba(156,39,176,0.55); box-shadow: 0 0 10px rgba(156,39,176,0.18); }
.ke-style-btn.act[data-style=brilliant] .ke-sl { color: #ce93d8; }
.ke-style-btn.act[data-style=endgame] { background: rgba(255,193,7,0.12); border-color: rgba(255,193,7,0.50); box-shadow: 0 0 10px rgba(255,193,7,0.15); }
.ke-style-btn.act[data-style=endgame] .ke-sl { color: #ffc107; }
.ke-style-btn.act[data-style=positional] { background: rgba(0,188,212,0.12); border-color: rgba(0,188,212,0.50); box-shadow: 0 0 10px rgba(0,188,212,0.15); }
.ke-style-btn.act[data-style=positional] .ke-sl { color: #00bcd4; }
/* Description bar below grid */
.ke-style-desc {
font-size: 9px; color: #555; margin-bottom: 10px;
padding: 5px 8px; background: #111115;
border: 1px solid rgba(255,255,255,0.05); border-radius: 5px;
min-height: 26px; line-height: 1.4;
transition: color 0.2s;
}
.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; }
.ke-footer { padding: 7px 12px; border-top: 1px solid rgba(255,255,255,0.05); display: flex; gap: 8px; font-size: 9.5px; color: #444; flex-wrap: wrap; }
.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; }
.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-stream-btn" id="ke-stream-btn">
<div class="ke-stream-btn-bg"></div>
<div class="ke-stream-icon">📡</div>
<div class="ke-stream-txt">
<div class="ke-stream-title" id="ke-stream-title">STREAM HIDE</div>
<div class="ke-stream-sub" id="ke-stream-sub">Hides panel, shows mini HUD — drag it off-capture</div>
</div>
<div class="ke-stream-badge" id="ke-stream-badge">OFF</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>
<!-- ═══ PLAY TAB ═══════════════════════════════════════════ -->
<div class="ke-page act" id="ke-p-play">
<!-- Playing Style grid (NEW) -->
<div class="ke-style-section-lbl">Playing Style</div>
<div class="ke-style-grid" id="ke-style-grid">
<div class="ke-style-btn act" data-style="balanced">
<span class="ke-si">⚖️</span>
<span class="ke-sl">Balanced</span>
</div>
<div class="ke-style-btn" data-style="aggressive">
<span class="ke-si">🔥</span>
<span class="ke-sl">Aggressive</span>
</div>
<div class="ke-style-btn" data-style="defensive">
<span class="ke-si">🛡️</span>
<span class="ke-sl">Defensive</span>
</div>
<div class="ke-style-btn" data-style="brilliant">
<span class="ke-si">✨</span>
<span class="ke-sl">Brilliant</span>
</div>
<div class="ke-style-btn" data-style="endgame">
<span class="ke-si">👑</span>
<span class="ke-sl">Endgame</span>
</div>
<div class="ke-style-btn" data-style="positional">
<span class="ke-si">♟️</span>
<span class="ke-sl">Positional</span>
</div>
</div>
<div class="ke-style-desc" id="ke-style-desc">Normal humanised engine play</div>
<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>
<!-- ═══ TUNE TAB ═══════════════════════════════════════════ -->
<div class="ke-page" id="ke-p-tune">
<div class="ke-elo-block" id="ke-elo-block">
<div class="ke-elo-hdr">
<span class="ke-elo-title">🎯 ELO STRENGTH MODE</span>
<div class="ke-sw elo-sw" id="sw-elo"></div>
</div>
<div class="ke-elo-input-row">
<input type="number" class="ke-elo-input" id="elo-input" min="500" max="3200" step="50" value="${CFG.targetElo}">
<div class="ke-elo-badge" id="elo-badge">${EloStrength.label(CFG.targetElo)}</div>
</div>
<div class="ke-elo-bar-wrap">
<div class="ke-elo-bar" id="elo-bar" style="width:${((CFG.targetElo-500)/2700*100).toFixed(1)}%"></div>
</div>
<div class="ke-elo-ticks"><span>500</span><span>1200</span><span>1800</span><span>2400</span><span>3200</span></div>
<div class="ke-elo-presets">
<div class="ke-elo-preset" data-elo="800">800</div>
<div class="ke-elo-preset" data-elo="1200">1200</div>
<div class="ke-elo-preset act" data-elo="1500">1500</div>
<div class="ke-elo-preset" data-elo="1800">1800</div>
<div class="ke-elo-preset" data-elo="2200">2200</div>
<div class="ke-elo-preset" data-elo="2700">2700</div>
</div>
<div style="font-size:9.5px;color:#555;letter-spacing:0.08em;text-transform:uppercase;margin:9px 0 5px">ELO Time Control</div>
<div class="ke-tc-row" id="elo-tc-row">
<div class="ke-tc ${CFG.eloTc==='bullet'?'act':''}" data-elotc="bullet">🔴 Bullet</div>
<div class="ke-tc ${CFG.eloTc==='blitz'?'act':''}" data-elotc="blitz">🟡 Blitz</div>
<div class="ke-tc ${CFG.eloTc==='rapid'?'act':''}" data-elotc="rapid">🟢 Rapid</div>
</div>
<div id="elo-timing-preview" style="font-size:9px;color:#555;margin-top:5px;text-align:center"></div>
</div>
<div id="ke-manual-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>
<div class="ke-row"><span class="ke-rlbl">Humanization</span><div class="ke-sw ${CFG.humanization?'on':''}" id="sw-hum"></div></div>
</div>
<!-- ═══ STATS TAB ══════════════════════════════════════════ -->
<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 class="ke-stat" style="margin-top:6px">
<div class="ke-stat-l">Active ELO</div>
<div class="ke-stat-v" id="st-elo" style="font-size:13px;color:#ce93d8">—</div>
</div>
<div class="ke-stat" style="margin-top:6px">
<div class="ke-stat-l">Playing Style</div>
<div class="ke-stat-v" id="st-style" style="font-size:13px;color:#9e9e9e">⚖️ Balanced</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">S</span> Stream</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);
UI._updateEloUI(CFG.targetElo, false);
},
_refreshStreamBtn: () => {
const p = UI.panel;
if (!p) return;
const btn = p.querySelector('#ke-stream-btn');
const title = p.querySelector('#ke-stream-title');
const sub = p.querySelector('#ke-stream-sub');
const badge = p.querySelector('#ke-stream-badge');
const dot = p.querySelector('#ke-dot');
if (!btn) return;
btn.classList.toggle('on', StreamHide.active);
if (title) title.textContent = StreamHide.active ? 'STREAM HIDE ON' : 'STREAM HIDE';
if (sub) sub.textContent = StreamHide.active ? 'Mini HUD active — drag it outside capture region' : 'Hidden from recordings';
if (badge) badge.textContent = StreamHide.active ? 'ON' : 'OFF';
if (dot && CFG.active) dot.className = 'ke-dot ' + (StreamHide.active ? 'stream' : CFG.eloMode ? 'elo' : 'active');
},
// ── Update style selector UI ─────────────────────────────────────────
_updateStyleUI: (style) => {
const p = UI.panel;
if (!p) return;
p.querySelectorAll('.ke-style-btn').forEach(b => b.classList.toggle('act', b.dataset.style === style));
const desc = p.querySelector('#ke-style-desc');
if (desc) desc.textContent = PlayStyle.describe(style);
const stStyle = p.querySelector('#st-style');
if (stStyle) {
const sl = PlayStyle.LABELS[style] || PlayStyle.LABELS.balanced;
stStyle.textContent = sl.icon + ' ' + sl.label;
stStyle.style.color = sl.color;
}
},
_updateEloUI: (elo, modeOn) => {
const p = UI.panel;
if (!p) return;
const input = p.querySelector('#elo-input');
const badge = p.querySelector('#elo-badge');
const bar = p.querySelector('#elo-bar');
const block = p.querySelector('#ke-elo-block');
const manual = p.querySelector('#ke-manual-tune');
const stElo = p.querySelector('#st-elo');
const sw = p.querySelector('#sw-elo');
const presets = p.querySelectorAll('.ke-elo-preset');
const dot = p.querySelector('#ke-dot');
const preview = p.querySelector('#elo-timing-preview');
if (input) input.value = elo;
if (badge) badge.textContent = EloStrength.label(elo);
if (bar) bar.style.width = ((elo - 500) / 2700 * 100).toFixed(1) + '%';
if (block) block.classList.toggle('elo-on', modeOn);
if (sw) sw.classList.toggle('on', modeOn);
if (manual) { manual.style.opacity = modeOn ? '0.35' : '1'; manual.style.pointerEvents = modeOn ? 'none' : ''; }
if (stElo) stElo.textContent = modeOn ? elo + ' (' + EloStrength.label(elo) + ')' : '—';
presets.forEach(pr => pr.classList.toggle('act', parseInt(pr.dataset.elo) === elo));
p.querySelectorAll('[data-elotc]').forEach(b => b.classList.toggle('act', b.dataset.elotc === CFG.eloTc));
if (preview) {
const t = EloStrength.toTiming(elo, CFG.eloTc);
const skl = EloStrength.toSkill(elo);
const dep = EloStrength.toDepth(elo);
preview.textContent = (t.min/1000).toFixed(1) + '–' + (t.max/1000).toFixed(1) + 's per move · Skill ' + skl + ' · Depth ' + dep;
}
if (dot && CFG.active && !StreamHide.active) dot.className = 'ke-dot ' + (modeOn ? 'elo' : 'active');
},
_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
? (CFG.eloMode ? 'ELO ' + CFG.targetElo + ' ' + CFG.eloTc + ' — ' + EloStrength.label(CFG.targetElo) : 'Analysing every move')
: 'Engine ready';
const dot = p.querySelector('#ke-dot');
if (dot) dot.className = 'ke-dot ' + (CFG.active ? (StreamHide.active ? 'stream' : CFG.eloMode ? 'elo' : 'active') : (SF.ready ? 'ready' : 'loading'));
if (CFG.active) {
State.lastFen = null; Loop._lastAnalyzedFen = null; SF._pendingFen = null;
if (CFG.eloMode) EloStrength.apply(CFG.targetElo);
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});
// Stream Hide button
const streamBtn = p.querySelector('#ke-stream-btn');
if (streamBtn) {
streamBtn.addEventListener('click', (e) => { e.stopPropagation(); StreamHide.toggle(); });
streamBtn.addEventListener('touchend', (e) => { e.preventDefault(); e.stopPropagation(); StreamHide.toggle(); }, { passive: false });
}
// ── PLAYING STYLE buttons (NEW) ─────────────────────────────────
p.querySelectorAll('.ke-style-btn').forEach(btn => {
btn.addEventListener('click', () => {
const newStyle = btn.dataset.style;
CFG.style = newStyle;
UI._updateStyleUI(newStyle);
log('Playing style → ' + newStyle);
// Force re-analysis so next move immediately uses new style
if (CFG.active && State.lastFen) {
Loop._lastAnalyzedFen = null;
State.lastFen = null;
requestAnimationFrame(Loop.tick);
}
});
});
// 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');
});
});
// Basic 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');
// ELO switch
const eloSwitch = p.querySelector('#sw-elo');
if (eloSwitch) {
eloSwitch.addEventListener('click', () => {
CFG.eloMode = !CFG.eloMode;
UI._updateEloUI(CFG.targetElo, CFG.eloMode);
if (CFG.eloMode) {
EloStrength.apply(CFG.targetElo);
if (CFG.active) p.querySelector('#ke-msub').textContent = 'ELO ' + CFG.targetElo + ' ' + CFG.eloTc + ' — ' + EloStrength.label(CFG.targetElo);
} else {
EloStrength.reset();
if (CFG.active) p.querySelector('#ke-msub').textContent = 'Analysing every move';
}
if (CFG.active && State.lastFen) { Loop._lastAnalyzedFen = null; State.lastFen = null; requestAnimationFrame(Loop.tick); }
});
}
// ELO input
const eloInput = p.querySelector('#elo-input');
if (eloInput) {
const applyEloInput = () => {
let v = parseInt(eloInput.value) || 1500;
v = Math.round(Math.max(500, Math.min(3200, v)) / 50) * 50;
CFG.targetElo = v;
UI._updateEloUI(v, CFG.eloMode);
if (CFG.eloMode) {
EloStrength.apply(v);
if (CFG.active) { p.querySelector('#ke-msub').textContent = 'ELO ' + v + ' ' + CFG.eloTc + ' — ' + EloStrength.label(v); Loop._lastAnalyzedFen = null; State.lastFen = null; requestAnimationFrame(Loop.tick); }
}
};
eloInput.addEventListener('change', applyEloInput);
eloInput.addEventListener('keydown', e => { if (e.key === 'Enter') { applyEloInput(); eloInput.blur(); } e.stopPropagation(); });
}
// ELO presets
p.querySelectorAll('.ke-elo-preset').forEach(btn => {
btn.addEventListener('click', () => {
const v = parseInt(btn.dataset.elo);
CFG.targetElo = v;
UI._updateEloUI(v, CFG.eloMode);
if (CFG.eloMode) {
EloStrength.apply(v);
if (CFG.active) { p.querySelector('#ke-msub').textContent = 'ELO ' + v + ' ' + CFG.eloTc + ' — ' + EloStrength.label(v); Loop._lastAnalyzedFen = null; State.lastFen = null; requestAnimationFrame(Loop.tick); }
}
});
});
// ELO TC buttons
p.querySelectorAll('[data-elotc]').forEach(b => {
b.addEventListener('click', () => {
CFG.eloTc = b.dataset.elotc;
UI._updateEloUI(CFG.targetElo, CFG.eloMode);
log('ELO TC → ' + CFG.eloTc);
});
});
// TC buttons
p.querySelectorAll('.ke-tc').forEach(b => {
b.addEventListener('click', () => {
if (!b.dataset.tc) return; // guard — ELO TC buttons also in this class
CFG.timeControl = b.dataset.tc;
p.querySelectorAll('[data-tc]').forEach(x => x.classList.toggle('act', x.dataset.tc === b.dataset.tc));
if (!CFG.eloMode) {
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 shortcuts
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 === 's') StreamHide.toggle();
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 && !StreamHide.active) dot.className = 'ke-dot ' + (CFG.active && CFG.eloMode ? 'elo' : 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 || StreamHide.active) 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)), 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 color reflects active style
const sl = PlayStyle.LABELS[CFG.style] || PlayStyle.LABELS.balanced;
const bestColor = CFG.style === 'balanced' ? '#4caf50' : sl.color;
arrow(bestMove, bestColor, 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; }
let tries = 0;
while (!Board.el() && tries++ < 30) await sleep(400);
if (!Board.el()) { log('Board not found', 'error'); return; }
SF.init().then(ok => {
if (ok) { log('Engine ready — press E or click the button'); UI.setStatus('ready'); }
});
Loop.start();
const redraw = () => {
if (!CFG.active || _paused || StreamHide.active) 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 });
})();
})();