Chess.com cheat engine with advanced humanization
// ==UserScript==
// @name Chess.com Cheat Engine
// @namespace http://tampermonkey.net/
// @version 11.0
// @description Chess.com cheat engine with advanced humanization
// @author rexxx
// @license MIT
// @match https://www.chess.com/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect unpkg.com
// @connect cdn.jsdelivr.net
// @connect cdnjs.cloudflare.com
// @connect lichess.org
// @connect explorer.lichess.ovh
// @connect stockfish.online
// @connect tablebase.lichess.ovh
// @connect tablebase.lichess.ovh
// @run-at document-idle
// @grant unsafeWindow
// ==/UserScript==
(function () {
'use strict';
const CONFIG = {
// Engine settings - LOWER depth = less perfect play = harder to detect
engineDepth: {
base: 12,
min: 8,
max: 16,
dynamicDepth: true,
},
multiPV: 5, // Get top 5 candidates for more suboptimal variety
pollInterval: 1000,
// --- HUMANIZATION CORE ---
humanization: {
enabled: true,
// Chess.com flags engine correlation by rating bracket
// 1000-1500: >65% is suspicious. 1500-2000: >72% is suspicious
// Target 55-62% to look like a strong-but-human player
targetEngineCorrelation: 0.58,
// How often to pick 2nd/3rd/4th best move instead of best
suboptimalMoveRate: {
opening: 0.20, // 20% in opening
middlegame: 0.40, // 40% in middlegame (humans make most errors here)
endgame: 0.28, // 28% in endgame
},
// When winning big, play MORE suboptimal (humans get lazy/overconfident)
winningDegradation: {
enabled: true,
tiers: [
{ evalAbove: 1.0, extraSuboptimalRate: 0.08 },
{ evalAbove: 2.0, extraSuboptimalRate: 0.18 },
{ evalAbove: 3.5, extraSuboptimalRate: 0.30 },
{ evalAbove: 5.0, extraSuboptimalRate: 0.40 },
{ evalAbove: 8.0, extraSuboptimalRate: 0.55 },
],
},
// When losing, play sharper (humans try harder when behind)
losingSharpness: {
enabled: true,
evalBelow: -0.8,
suboptimalReduction: 0.40, // reduce suboptimal rate by 60% when losing
},
// Maximum centipawn loss for "suboptimal" moves
maxAcceptableCPLoss: {
opening: 55, // slightly bigger inaccuracies
middlegame: 110, // moderate-large inaccuracies in middlegame
endgame: 65, // moderate in endgame
},
// Genuine blunders (context-dependent)
blunder: {
chance: 0.045, // 4.5% base chance (real humans blunder ~5-8%)
onlyInComplexPositions: true,
maxCPLoss: 200,
// Only blunder when we're safely ahead
disableWhenEvalBetween: [-2.0, 2.0],
},
// Streaks - shorter perfect runs before forced slip
streaks: {
enabled: true,
perfectStreakMax: 4, // after 4 perfect moves, force a suboptimal
sloppyStreakMax: 3, // after 3 suboptimal, play best for a while
},
// Per-game personality variance (WIDER variance = harder to fingerprint)
personalityVariance: {
enabled: true,
suboptimalRateJitter: 0.30, // +/- 30% on suboptimal rates
depthJitter: 3, // +/- 3 depth variance per game
timingJitter: 0.40, // +/- 40% on timing
},
},
// --- TIMING / HUMANIZER ---
// Chess.com analyzes move time distributions. Uniform random is detectable.
// Real humans have log-normal distribution (cluster around median, long tail)
timing: {
// Base delays (higher = safer)
base: { min: 800, max: 3500 },
// Fast recaptures / forced moves
forced: { min: 250, max: 900 },
// Opening book moves
book: { min: 500, max: 2000 },
// Complex positions
complex: { min: 2500, max: 7000 },
// Simple/winning positions
simple: { min: 600, max: 1800 },
// Occasional long think (simulates deep calculation)
longThink: { chance: 0.10, min: 5000, max: 12000 },
// Occasional instant move (pre-move / obvious reply)
instantMove: { chance: 0.05, min: 150, max: 400 },
// Fatigue: moves get slower as game progresses
fatigue: {
enabled: true,
startMove: 20,
msPerMove: 25,
cap: 1200,
},
},
// --- SESSION MANAGEMENT (anti-pattern detection) ---
session: {
maxGamesPerSession: 12, // take a break after N games
breakDurationMs: 180000, // 3 minute break between sessions
maxWinStreak: 6, // after N wins, intentionally lose/draw one
gamesPlayed: 0,
wins: 0,
currentWinStreak: 0,
},
auto: {
enabled: false,
autoQueue: true,
},
arrowOpacity: 0.8,
showPanel: true,
showThreats: true,
stealthMode: false,
useBook: true,
};
const SELECTORS = {
board: ['wc-chess-board', 'chess-board'],
chat: '.chat-scroll-area-component',
moves: 'vertical-move-list, wc-move-list, .move-list-component',
clocks: '.clock-component',
gameOver: [
'.game-over-modal-container',
'.modal-game-over-component',
'[data-cy="game-over-modal"]',
'.game-over-modal-buttons',
'.game-over-buttons-component',
'.game-result-header'
],
drawOffer: '.draw-offer-component',
promotion: {
dialog: '.promotion-window, .promotion-piece',
items: '.promotion-piece'
}
};
const State = {
engineFound: false,
isThinking: false,
lastFen: null,
playerColor: null,
gameId: null,
moveCount: 0,
boredomLevel: 0,
personality: null,
ui: {
overlay: null,
panel: null,
statusDot: null,
autoIndicator: null
},
workers: {
stockfish: null
},
cache: {
fen: new Map(),
board: null
},
// MultiPV candidate tracking
candidates: {}, // { pvIndex: { move, eval: {type, value}, depth } }
currentEval: null,
currentBestMove: null,
opponentResponse: null,
// Humanization tracking
human: {
perfectStreak: 0, // consecutive best-move plays
sloppyStreak: 0, // consecutive suboptimal plays
bestMoveCount: 0, // total best moves this game
totalMoveCount: 0, // total moves this game
gamePersonality: null, // randomized per-game modifiers
lastMoveWasBest: true,
},
};
const Utils = {
sleep: (ms) => new Promise(r => setTimeout(r, ms)),
log: (msg, type = 'info') => {
const colors = {
info: '#3eff3e',
warn: '#ffcc00',
error: '#ff4444',
debug: '#aaaaff'
};
console.log(`%c[BA] ${msg}`, `color: ${colors[type]}; font-weight: bold;`);
},
randomRange: (min, max) => Math.random() * (max - min) + min,
// Gaussian random using Box-Muller transform
gaussianRandom: (mean = 0, stdDev = 1) => {
const u1 = Math.random();
const u2 = Math.random();
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
return mean + z * stdDev;
},
// Log-normal delay: clusters around median with occasional long thinks
// This matches real human move-time distributions
humanDelay: (min, max) => {
const median = (min + max) / 2;
const sigma = 0.6; // controls spread (higher = more variance)
const logNormal = Math.exp(Utils.gaussianRandom(Math.log(median), sigma));
// Clamp to reasonable range but allow occasional outliers
return Math.max(min * 0.7, Math.min(max * 1.4, logNormal));
},
isTabActive: () => !document.hidden,
query: (selector, root = document) => {
if (Array.isArray(selector)) {
for (const s of selector) {
const el = root.querySelector(s);
if (el) return el;
}
return null;
}
return root.querySelector(selector);
}
};
const UI = {
injectStyles: () => {
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;700&display=swap');
.ba-overlay { pointer-events: none; z-index: 10000; position: absolute; top: 0; left: 0; transition: opacity 0.2s; }
.ba-stealth { opacity: 0 !important; pointer-events: none !important; }
.ba-panel {
position: fixed; top: 50px; left: 50px; z-index: 10001;
width: 300px;
background: rgba(10, 10, 12, 0.95);
color: #e0e0e0;
border: 1px solid #333;
border-left: 2px solid #4caf50;
border-radius: 8px;
font-family: 'Inter', sans-serif;
box-shadow: 0 10px 40px rgba(0,0,0,0.6);
overflow: hidden;
display: flex; flex-direction: column;
}
.ba-header {
padding: 12px 16px;
background: linear-gradient(90deg, rgba(76,175,80,0.1), transparent);
border-bottom: 1px solid rgba(255,255,255,0.05);
display: flex; justify-content: space-between; align-items: center;
cursor: grab; user-select: none;
}
.ba-logo { font-weight: 800; font-size: 14px; letter-spacing: 1px; color: #4caf50; }
.ba-logo span { color: #fff; opacity: 0.7; font-weight: 400; }
.ba-minimize { cursor: pointer; opacity: 0.5; transition: 0.2s; }
.ba-minimize:hover { opacity: 1; color: #fff; }
.ba-tabs { display: flex; background: rgba(0,0,0,0.2); }
.ba-tab {
flex: 1; text-align: center; padding: 10px 0;
font-size: 11px; font-weight: 600; color: #666;
cursor: pointer; transition: 0.2s;
border-bottom: 2px solid transparent;
}
.ba-tab:hover { color: #aaa; background: rgba(255,255,255,0.02); }
.ba-tab.active { color: #e0e0e0; border-bottom: 2px solid #4caf50; background: rgba(76,175,80,0.05); }
.ba-content { padding: 16px; min-height: 150px; max-height: 400px; overflow-y: auto; }
.ba-page { display: none; }
.ba-page.active { display: block; animation: fadeIn 0.2s; }
.ba-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.ba-label { font-size: 12px; color: #aaa; }
.ba-value { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: #4caf50; }
.ba-slider-container { margin-bottom: 14px; }
.ba-slider-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
.ba-slider {
-webkit-appearance: none; width: 100%; height: 4px;
background: #333; border-radius: 2px; outline: none;
}
.ba-slider::-webkit-slider-thumb {
-webkit-appearance: none; width: 12px; height: 12px;
background: #4caf50; border-radius: 50%; cursor: pointer;
box-shadow: 0 0 10px rgba(76,175,80,0.4);
}
.ba-checkbox {
width: 16px; height: 16px; border: 1px solid #444;
background: #111; border-radius: 3px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
}
.ba-checkbox.checked { background: #4caf50; border-color: #4caf50; }
.ba-checkbox.checked::after { content: '✓'; font-size: 10px; color: #000; font-weight: bold; }
.ba-status-box {
background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05);
border-radius: 6px; padding: 12px; margin-bottom: 16px; text-align: center;
}
.ba-eval-large { font-family: 'JetBrains Mono'; font-size: 24px; font-weight: 700; color: #fff; margin-bottom: 4px; display: block;}
.ba-best-move-large { font-family: 'JetBrains Mono'; font-size: 14px; color: #4caf50; background: rgba(76,175,80,0.1); padding: 4px 8px; border-radius: 4px; display: inline-block; }
.ba-footer {
padding: 8px 16px; font-size: 10px; color: #555;
border-top: 1px solid rgba(255,255,255,0.05);
display: flex; gap: 12px;
}
.ba-key { color: #888; background: #222; padding: 1px 4px; border-radius: 3px; font-family: monospace; border: 1px solid #333; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
.ba-arrow { stroke-linecap: round; opacity: ${CONFIG.arrowOpacity}; filter: drop-shadow(0 0 4px rgba(0,0,0,0.5)); }
`);
},
createInterface: () => {
if (State.ui.panel) return;
const panel = document.createElement('div');
panel.className = 'ba-panel';
panel.innerHTML = `
<div class="ba-header">
<div class="ba-logo">REXXX<span>.MENU</span></div>
<div class="ba-minimize">_</div>
</div>
<div class="ba-tabs">
<div class="ba-tab active" data-tab="main">MAIN</div>
<div class="ba-tab" data-tab="timings">TIMINGS</div>
<div class="ba-tab" data-tab="visuals">VISUALS</div>
</div>
<div class="ba-content">
<div id="tab-main" class="ba-page active">
<div class="ba-status-box">
<span class="ba-eval-large">0.00</span>
<span class="ba-best-move-large">Waiting...</span>
</div>
<div class="ba-row">
<span class="ba-label">Auto-Play</span>
<div class="ba-checkbox ${CONFIG.auto.enabled ? 'checked' : ''}" id="toggle-auto"></div>
</div>
<div class="ba-row">
<span class="ba-label">Opening Book</span>
<div class="ba-checkbox ${CONFIG.useBook ? 'checked' : ''}" id="toggle-book"></div>
</div>
</div>
<div id="tab-timings" class="ba-page">
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Engine Correlation %</span>
<span class="ba-value" id="val-corr">${Math.round(CONFIG.humanization.targetEngineCorrelation * 100)}</span>
</div>
<input type="range" class="ba-slider" min="50" max="95" value="${Math.round(CONFIG.humanization.targetEngineCorrelation * 100)}" id="slide-corr">
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Base Min Delay (ms)</span>
<span class="ba-value" id="val-min">${CONFIG.timing.base.min}</span>
</div>
<input type="range" class="ba-slider" min="200" max="3000" value="${CONFIG.timing.base.min}" id="slide-min">
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Base Max Delay (ms)</span>
<span class="ba-value" id="val-max">${CONFIG.timing.base.max}</span>
</div>
<input type="range" class="ba-slider" min="500" max="8000" value="${CONFIG.timing.base.max}" id="slide-max">
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Middlegame Suboptimal %</span>
<span class="ba-value" id="val-subopt">${Math.round(CONFIG.humanization.suboptimalMoveRate.middlegame * 100)}</span>
</div>
<input type="range" class="ba-slider" min="5" max="50" value="${Math.round(CONFIG.humanization.suboptimalMoveRate.middlegame * 100)}" id="slide-subopt">
</div>
</div>
<div id="tab-visuals" class="ba-page">
<div class="ba-row">
<span class="ba-label">Arrow Opacity</span>
<div class="ba-slider" style="width: 100px;"></div>
</div>
<div class="ba-row">
<span class="ba-label">Show Threats</span>
<div class="ba-checkbox ${CONFIG.showThreats ? 'checked' : ''}" id="toggle-threats"></div>
</div>
</div>
</div>
<div class="ba-footer">
<span><span class="ba-key">A</span> Toggle Auto</span>
<span><span class="ba-key">X</span> Stealth</span>
</div>
`;
document.body.appendChild(panel);
State.ui.panel = panel;
UI.makeDraggable(panel);
UI.initListeners(panel);
},
initListeners: (panel) => {
panel.querySelectorAll('.ba-tab').forEach(t => {
t.addEventListener('click', (e) => {
panel.querySelectorAll('.ba-tab').forEach(x => x.classList.remove('active'));
panel.querySelectorAll('.ba-page').forEach(x => x.classList.remove('active'));
e.target.classList.add('active');
panel.querySelector(`#tab-${e.target.dataset.tab}`).classList.add('active');
});
});
const toggle = (id, configPath, callback) => {
panel.querySelector(`#${id}`).addEventListener('click', (e) => {
const keys = configPath.split('.');
if (keys.length === 2) CONFIG[keys[0]][keys[1]] = !CONFIG[keys[0]][keys[1]];
else CONFIG[configPath] = !CONFIG[configPath];
e.target.classList.toggle('checked');
if (callback) callback();
});
};
toggle('toggle-auto', 'auto.enabled');
toggle('toggle-book', 'useBook');
toggle('toggle-threats', 'showThreats');
const slider = (id, valId, setter) => {
const el = panel.querySelector(`#${id}`);
const display = panel.querySelector(`#${valId}`);
if (!el || !display) return;
el.addEventListener('input', (e) => {
const val = parseInt(e.target.value);
setter(val);
display.textContent = val;
});
};
slider('slide-corr', 'val-corr', (v) => { CONFIG.humanization.targetEngineCorrelation = v / 100; });
slider('slide-min', 'val-min', (v) => { CONFIG.timing.base.min = v; });
slider('slide-max', 'val-max', (v) => { CONFIG.timing.base.max = v; });
slider('slide-subopt', 'val-subopt', (v) => { CONFIG.humanization.suboptimalMoveRate.middlegame = v / 100; });
},
makeDraggable: (el) => {
const header = el.querySelector('.ba-header');
let isDragging = false;
let startX, startY, initialLeft, initialTop;
header.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
initialLeft = el.offsetLeft;
initialTop = el.offsetTop;
header.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
el.style.left = `${initialLeft + dx}px`;
el.style.top = `${initialTop + dy}px`;
});
document.addEventListener('mouseup', () => {
isDragging = false;
header.style.cursor = 'grab';
});
},
toggleStealth: () => {
CONFIG.stealthMode = !CONFIG.stealthMode;
const p = State.ui.panel;
if (p) p.style.opacity = CONFIG.stealthMode ? '0' : '1';
document.querySelectorAll('.ba-overlay').forEach(el => {
el.classList.toggle('ba-stealth', CONFIG.stealthMode);
});
},
updatePanel: (evalData, bestMove) => {
if (!State.ui.panel) return;
const evalBox = State.ui.panel.querySelector('.ba-eval-large');
const moveBox = State.ui.panel.querySelector('.ba-best-move-large');
if (evalData) {
evalBox.textContent = evalData.type === 'mate' ? `M${evalData.value}` : evalData.value.toFixed(2);
evalBox.style.color = evalData.value > 0.5 ? '#4caf50' : (evalData.value < -0.5 ? '#ff5252' : '#e0e0e0');
}
if (bestMove) {
moveBox.textContent = bestMove.move || '...';
}
State.ui.panel.querySelector('#toggle-auto').classList.toggle('checked', CONFIG.auto.enabled);
},
updateStatus: (color) => {
if (!State.ui.panel) return;
State.ui.panel.style.borderLeftColor = color;
const logo = State.ui.panel.querySelector('.ba-logo');
if (logo) logo.style.color = color;
if (color === 'red') {
const moveBox = State.ui.panel.querySelector('.ba-best-move-large');
if (moveBox) moveBox.textContent = 'Engine Failed';
}
},
clearOverlay: () => {
document.querySelectorAll('.ba-overlay').forEach(e => e.remove());
},
drawMove: (move, color = '#4caf50', secondary = false) => {
if (CONFIG.stealthMode) return;
if (!secondary) UI.clearOverlay();
const board = Game.getBoard();
if (!board) return;
const rect = board.getBoundingClientRect();
let overlay = secondary ? document.querySelector('.ba-overlay') : null;
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'ba-overlay';
if (CONFIG.stealthMode) overlay.classList.add('ba-stealth');
overlay.style.width = rect.width + 'px';
overlay.style.height = rect.height + 'px';
overlay.style.left = rect.left + window.scrollX + 'px';
overlay.style.top = rect.top + window.scrollY + 'px';
document.body.appendChild(overlay);
}
const from = move.substring(0, 2);
const to = move.substring(2, 4);
const file = (c) => c.charCodeAt(0) - 97;
const rank = (c) => parseInt(c) - 1;
const isFlipped = State.playerColor === 'b';
const sqSize = rect.width / 8;
const getPos = (sq) => {
const f = file(sq[0]);
const r = rank(sq[1]);
return {
x: (isFlipped ? 7 - f : f) * sqSize + sqSize / 2,
y: (isFlipped ? r : 7 - r) * sqSize + sqSize / 2
};
};
const p1 = getPos(from);
const p2 = getPos(to);
let svg = overlay.querySelector('svg');
if (!svg) {
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.style.width = '100%';
svg.style.height = '100%';
overlay.appendChild(svg);
}
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', p1.x);
line.setAttribute('y1', p1.y);
line.setAttribute('x2', p2.x);
line.setAttribute('y2', p2.y);
line.setAttribute('stroke', color);
line.setAttribute('stroke-width', sqSize * (secondary ? 0.1 : 0.18));
line.setAttribute('class', 'ba-arrow');
if (secondary) line.setAttribute('stroke-dasharray', '5,5');
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', p2.x);
circle.setAttribute('cy', p2.y);
circle.setAttribute('r', sqSize * (secondary ? 0.1 : 0.18));
circle.setAttribute('fill', color);
circle.setAttribute('opacity', secondary ? 0.5 : 0.8);
svg.appendChild(line);
svg.appendChild(circle);
}
};
const Game = {
getBoard: () => {
if (State.cache.board && State.cache.board.isConnected) return State.cache.board;
State.cache.board = Utils.query(SELECTORS.board);
return State.cache.board;
},
getElementAtSquare: (sq) => {
const board = Game.getBoard();
if (!board) return null;
const piece = board.querySelector(`.piece.square-${Game.squareToCoords(sq)}`) ||
board.querySelector(`.piece.${sq}`);
if (piece) return piece;
if (board.shadowRoot) {
const shadowPiece = board.shadowRoot.querySelector(`.piece.square-${Game.squareToCoords(sq)}`);
if (shadowPiece) return shadowPiece;
}
return board;
},
squareToCoords: (sq) => {
const file = sq.charCodeAt(0) - 96;
const rank = sq[1];
return `${file}${rank}`;
},
detectColor: () => {
const board = Game.getBoard();
if (!board) return null;
const isFlipped = board.classList.contains('flipped') || board.getAttribute('flipped') === 'true';
return isFlipped ? 'b' : 'w';
},
isValidFen: (fen) => {
if (!fen || typeof fen !== 'string') return false;
return fen.split(' ').length >= 4;
},
getFen: () => {
const board = Game.getBoard();
if (!board) return null;
if (board.game && board.game.getFEN) return board.game.getFEN();
const keys = Object.keys(board);
const reactKey = keys.find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternal'));
if (reactKey) {
let curr = board[reactKey];
while (curr) {
if (curr.memoizedProps?.game?.fen) return curr.memoizedProps.game.fen;
if (typeof curr.memoizedProps?.fen === 'string') return curr.memoizedProps.fen;
curr = curr.return;
}
}
return null;
},
isMyTurn: (fen) => {
if (!fen || !State.playerColor) return false;
const turn = fen.split(' ')[1];
return turn === State.playerColor;
},
isCapture: (move) => {
const board = Game.getBoard();
if (!board) return false;
const to = move.substring(2, 4);
const coords = Game.squareToCoords(to);
return !!board.querySelector(`.piece.square-${coords}`);
}
};
const OpeningBook = {
fetchMove: (fen) => {
if (!CONFIG.useBook) return null;
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://explorer.lichess.ovh/masters?fen=${fen}`,
onload: (response) => {
try {
const data = JSON.parse(response.responseText);
if (data.moves && data.moves.length > 0) {
const topMoves = data.moves.slice(0, 3);
const totalGames = topMoves.reduce((sum, m) => sum + m.white + m.draw + m.black, 0);
let r = Math.random() * totalGames;
for (const move of topMoves) {
const games = move.white + move.draw + move.black;
if (r < games) {
resolve(move.uci);
return;
}
r -= games;
}
resolve(topMoves[0].uci);
} else {
resolve(null);
}
} catch (e) {
resolve(null);
}
},
onerror: () => resolve(null)
});
});
}
};
const HumanStrategy = {
// Detect game phase from FEN
getGamePhase: (fen) => {
if (!fen) return 'middlegame';
const board = fen.split(' ')[0];
const moveNum = State.moveCount;
// Count pieces (excluding pawns and kings)
const minorMajor = (board.match(/[rnbqRNBQ]/g) || []).length;
const queens = (board.match(/[qQ]/g) || []).length;
if (moveNum <= 12) return 'opening';
if (minorMajor <= 6 || (queens === 0 && minorMajor <= 8)) return 'endgame';
return 'middlegame';
},
// Count pieces for position complexity estimation
getPositionComplexity: (fen) => {
if (!fen) return 0.5;
const board = fen.split(' ')[0];
const pieces = (board.match(/[rnbqRNBQ]/g) || []).length;
const pawns = (board.match(/[pP]/g) || []).length;
// More pieces + more pawns = more complex
// Also consider if eval is close (tactical complexity)
let complexity = (pieces + pawns * 0.5) / 24; // normalized 0-1
if (State.currentEval && Math.abs(State.currentEval.value) < 0.5) {
complexity += 0.2; // equal positions are harder
}
return Math.min(1, Math.max(0, complexity));
},
// Randomize personality at game start for variance across games
initGamePersonality: () => {
const pv = CONFIG.humanization.personalityVariance;
if (!pv.enabled) {
State.human.gamePersonality = { suboptimalMult: 1, depthOffset: 0, timingMult: 1 };
return;
}
State.human.gamePersonality = {
suboptimalMult: 1 + (Math.random() * 2 - 1) * pv.suboptimalRateJitter,
depthOffset: Math.round((Math.random() * 2 - 1) * pv.depthJitter),
timingMult: 1 + (Math.random() * 2 - 1) * pv.timingJitter,
};
Utils.log(`Game Personality: subopt x${State.human.gamePersonality.suboptimalMult.toFixed(2)}, depth ${State.human.gamePersonality.depthOffset > 0 ? '+' : ''}${State.human.gamePersonality.depthOffset}, timing x${State.human.gamePersonality.timingMult.toFixed(2)}`, 'debug');
},
// Reset tracking for a new game
resetGame: () => {
State.human.perfectStreak = 0;
State.human.sloppyStreak = 0;
State.human.bestMoveCount = 0;
State.human.totalMoveCount = 0;
State.human.lastMoveWasBest = true;
State.moveCount = 0;
State.candidates = {};
HumanStrategy.initGamePersonality();
},
// Calculate dynamic engine depth for this move
getDynamicDepth: (fen) => {
const cfg = CONFIG.engineDepth;
if (!cfg.dynamicDepth) return cfg.base;
const complexity = HumanStrategy.getPositionComplexity(fen);
const phase = HumanStrategy.getGamePhase(fen);
const personality = State.human.gamePersonality || { depthOffset: 0 };
let depth = cfg.base + personality.depthOffset;
// Simpler positions -> slightly lower depth (engine finds best move fast)
// Complex positions -> higher depth
if (complexity < 0.3) depth -= 2;
else if (complexity > 0.7) depth += 2;
// Endgame: slightly higher depth for precision
if (phase === 'endgame') depth += 1;
// Add small random jitter per move (+/- 1)
depth += Math.round(Math.random() * 2 - 1);
return Math.max(cfg.min, Math.min(cfg.max, depth));
},
// Core decision: should we play the best move or pick a suboptimal one?
shouldPlaySuboptimal: (fen) => {
const h = CONFIG.humanization;
if (!h.enabled) return false;
const phase = HumanStrategy.getGamePhase(fen);
const personality = State.human.gamePersonality || { suboptimalMult: 1 };
const eval_ = State.currentEval;
// Get base suboptimal rate for this phase
let rate = h.suboptimalMoveRate[phase] || 0.25;
// Apply per-game personality jitter
rate *= personality.suboptimalMult;
// Winning degradation: play worse when winning big
if (h.winningDegradation.enabled && eval_) {
const adv = eval_.value; // positive = we're ahead
for (const tier of h.winningDegradation.tiers) {
if (adv >= tier.evalAbove) {
rate += tier.extraSuboptimalRate;
}
}
}
// Losing sharpness: play better when behind
if (h.losingSharpness.enabled && eval_ && eval_.value <= h.losingSharpness.evalBelow) {
rate *= h.losingSharpness.suboptimalReduction;
}
// Streak management: force variety in best/suboptimal patterns
if (h.streaks.enabled) {
if (State.human.perfectStreak >= h.streaks.perfectStreakMax) {
Utils.log('Streak: Forcing suboptimal after perfect run', 'debug');
return true; // force suboptimal after long perfect streak
}
if (State.human.sloppyStreak >= h.streaks.sloppyStreakMax) {
Utils.log('Streak: Forcing best after sloppy run', 'debug');
return false; // force best after sloppy streak
}
}
// Engine correlation guard: if we're playing too many best moves,
// increase suboptimal rate to keep correlation in target range
if (State.human.totalMoveCount >= 8) {
const currentCorrelation = State.human.bestMoveCount / State.human.totalMoveCount;
if (currentCorrelation > h.targetEngineCorrelation + 0.05) {
rate += 0.15; // significantly increase suboptimal chance
Utils.log(`Correlation guard: ${(currentCorrelation * 100).toFixed(0)}% > target, boosting suboptimal`, 'debug');
} else if (currentCorrelation < h.targetEngineCorrelation - 0.10) {
rate *= 0.3; // reduce suboptimal to raise correlation back up
Utils.log(`Correlation guard: ${(currentCorrelation * 100).toFixed(0)}% < target, reducing suboptimal`, 'debug');
}
}
// Clamp rate
rate = Math.max(0.05, Math.min(0.65, rate));
return Math.random() < rate;
},
// Pick the best viable suboptimal move from candidates
pickSuboptimalMove: (fen) => {
const phase = HumanStrategy.getGamePhase(fen);
const maxCPLoss = CONFIG.humanization.maxAcceptableCPLoss[phase] || 80;
const candidates = State.candidates;
const bestEval = candidates[1]?.eval;
if (!bestEval || Object.keys(candidates).length < 2) {
return candidates[1]?.move || null; // fallback to best
}
// Build list of acceptable alternative moves
const alternatives = [];
for (let i = 2; i <= CONFIG.multiPV; i++) {
const c = candidates[i];
if (!c || !c.eval) continue;
let cpLoss;
if (bestEval.type === 'mate' && c.eval.type === 'mate') {
// Both are mate: prefer shorter mate, but allow longer mate as "suboptimal"
cpLoss = Math.abs(c.eval.value - bestEval.value) * 50;
} else if (bestEval.type === 'mate') {
cpLoss = 200; // losing a forced mate is big
} else if (c.eval.type === 'mate' && c.eval.value > 0) {
cpLoss = 0; // alternative also gives mate - fine
} else {
cpLoss = (bestEval.value - c.eval.value) * 100;
}
if (cpLoss <= maxCPLoss && cpLoss >= 0) {
alternatives.push({ move: c.move, cpLoss, pvIndex: i });
}
}
if (alternatives.length === 0) {
Utils.log('No acceptable suboptimal moves, playing best', 'debug');
return null; // no acceptable alternative
}
// Weight selection: prefer smaller centipawn losses (more human-like)
// Humans usually play the 2nd best, rarely the 4th best
const weights = alternatives.map(a => 1 / (1 + a.cpLoss / 30));
const totalWeight = weights.reduce((s, w) => s + w, 0);
let r = Math.random() * totalWeight;
for (let i = 0; i < alternatives.length; i++) {
r -= weights[i];
if (r <= 0) {
Utils.log(`Suboptimal pick: PV${alternatives[i].pvIndex} (${alternatives[i].move}, -${alternatives[i].cpLoss.toFixed(0)}cp)`, 'warn');
return alternatives[i].move;
}
}
return alternatives[0].move;
},
// Check if we should blunder (very rare, context-dependent)
shouldBlunder: (fen) => {
const b = CONFIG.humanization.blunder;
if (!b || b.chance <= 0) return false;
const eval_ = State.currentEval;
if (!eval_) return false;
// Never blunder in close positions
if (eval_.value >= b.disableWhenEvalBetween[0] && eval_.value <= b.disableWhenEvalBetween[1]) {
return false;
}
// Only blunder in complex positions if configured
if (b.onlyInComplexPositions) {
const complexity = HumanStrategy.getPositionComplexity(fen);
if (complexity < 0.4) return false;
}
// Never blunder when losing (would make it worse)
if (eval_.value < 0) return false;
return Math.random() < b.chance;
},
// Pick the actual blunder move (worst acceptable candidate)
pickBlunderMove: () => {
const candidates = State.candidates;
const bestEval = candidates[1]?.eval;
if (!bestEval) return null;
const maxLoss = CONFIG.humanization.blunder.maxCPLoss;
// Find the worst move within the maxCPLoss limit
let worstMove = null;
let worstLoss = 0;
for (let i = 2; i <= CONFIG.multiPV; i++) {
const c = candidates[i];
if (!c || !c.eval) continue;
const cpLoss = (bestEval.value - c.eval.value) * 100;
if (cpLoss <= maxLoss && cpLoss > worstLoss) {
worstMove = c.move;
worstLoss = cpLoss;
}
}
if (worstMove) {
Utils.log(`BLUNDER: Playing ${worstMove} (-${worstLoss.toFixed(0)}cp)`, 'error');
}
return worstMove;
},
// Select the final move to play
selectMove: (fen, bestMove, isBook) => {
if (isBook || !CONFIG.humanization.enabled) {
return { move: bestMove, reason: isBook ? 'book' : 'engine', isBest: true };
}
// Check for blunder first (very rare)
if (HumanStrategy.shouldBlunder(fen)) {
const blunderMove = HumanStrategy.pickBlunderMove();
if (blunderMove) {
return { move: blunderMove, reason: 'blunder', isBest: false };
}
}
// Check if we should play suboptimal
if (HumanStrategy.shouldPlaySuboptimal(fen)) {
const subMove = HumanStrategy.pickSuboptimalMove(fen);
if (subMove) {
return { move: subMove, reason: 'suboptimal', isBest: false };
}
}
return { move: bestMove, reason: 'best', isBest: true };
},
// Track the move we played for correlation management
trackMove: (isBest) => {
State.human.totalMoveCount++;
if (isBest) {
State.human.bestMoveCount++;
State.human.perfectStreak++;
State.human.sloppyStreak = 0;
} else {
State.human.perfectStreak = 0;
State.human.sloppyStreak++;
}
State.human.lastMoveWasBest = isBest;
const corr = State.human.totalMoveCount > 0
? ((State.human.bestMoveCount / State.human.totalMoveCount) * 100).toFixed(0)
: '---';
Utils.log(`Correlation: ${corr}% (${State.human.bestMoveCount}/${State.human.totalMoveCount})`, 'debug');
},
// Calculate human-like delay for this move
calculateDelay: (fen, moveResult, isBook) => {
const t = CONFIG.timing;
const personality = State.human.gamePersonality || { timingMult: 1 };
let min, max;
if (isBook) {
min = t.book.min;
max = t.book.max;
} else if (Game.isCapture(moveResult.move) && State.currentEval && Math.abs(State.currentEval.value) > 2) {
// Obvious recapture when winning
min = t.forced.min;
max = t.forced.max;
Utils.log('Timing: Reflex capture');
} else if (Math.random() < t.instantMove.chance && State.moveCount > 5) {
// Pre-move / instant (simulates having pre-calculated)
min = t.instantMove.min;
max = t.instantMove.max;
Utils.log('Timing: Instant/pre-move');
} else if (Math.random() < t.longThink.chance) {
// Occasional long think
min = t.longThink.min;
max = t.longThink.max;
Utils.log('Timing: Long think');
} else {
const complexity = HumanStrategy.getPositionComplexity(fen);
if (complexity > 0.65) {
min = t.complex.min;
max = t.complex.max;
} else if (complexity < 0.3 || (State.currentEval && State.currentEval.value > 3)) {
min = t.simple.min;
max = t.simple.max;
} else {
min = t.base.min;
max = t.base.max;
}
// Equal eval -> think longer (humans struggle here)
if (State.currentEval && Math.abs(State.currentEval.value) < 0.3) {
min += 500;
max += 1200;
}
// If we're playing a suboptimal move, add slight hesitation
// (simulates "almost saw" the best move)
if (moveResult.reason === 'suboptimal') {
min += 200;
max += 600;
}
}
// Apply fatigue
if (t.fatigue.enabled && State.moveCount > t.fatigue.startMove) {
const extra = Math.min(t.fatigue.cap, (State.moveCount - t.fatigue.startMove) * t.fatigue.msPerMove);
min += extra;
max += extra;
}
// Apply per-game personality timing jitter
min *= personality.timingMult;
max *= personality.timingMult;
// Use log-normal distribution instead of uniform random
return Utils.humanDelay(min, max);
},
};
const Engine = {
init: async () => {
if (State.workers.stockfish) return;
Utils.log('Initializing Stockfish...');
UI.updateStatus('orange');
try {
const scriptContent = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://unpkg.com/[email protected]/stockfish.js',
onload: (res) => resolve(res.responseText),
onerror: (err) => reject(err)
});
});
const blob = new Blob([scriptContent], { type: 'application/javascript' });
State.workers.stockfish = new Worker(URL.createObjectURL(blob));
State.workers.stockfish.onmessage = (e) => {
const msg = e.data;
if (msg === 'uciok') {
State.engineFound = true;
UI.updateStatus('#4caf50');
Utils.log('Stockfish Ready');
}
if (msg.startsWith('bestmove')) {
const move = msg.split(' ')[1];
Engine.handleBestMove(move);
}
if (msg.startsWith('info') && msg.includes('score')) {
Engine.parseScore(msg);
}
};
State.workers.stockfish.postMessage('uci');
State.workers.stockfish.postMessage('isready');
State.workers.stockfish.postMessage(`setoption name MultiPV value ${CONFIG.multiPV}`);
HumanStrategy.initGamePersonality();
} catch (e) {
Utils.log('Stockfish Init Failed: ' + e, 'error');
UI.updateStatus('red');
}
},
analyze: async (fen) => {
if (!State.workers.stockfish || !State.engineFound) return;
State.isThinking = true;
State.candidates = {};
if (CONFIG.useBook) {
const bookMove = await OpeningBook.fetchMove(fen);
if (bookMove) {
Utils.log(`Book Move Found: ${bookMove}`);
Engine.handleBestMove(bookMove, true);
return;
}
}
const depth = HumanStrategy.getDynamicDepth(fen);
Utils.log(`Analyzing depth ${depth}, MultiPV ${CONFIG.multiPV}`, 'debug');
State.workers.stockfish.postMessage('stop');
State.workers.stockfish.postMessage(`position fen ${fen}`);
State.workers.stockfish.postMessage(`setoption name MultiPV value ${CONFIG.multiPV}`);
State.workers.stockfish.postMessage(`go depth ${depth}`);
},
parseScore: (msg) => {
const scoreMatch = msg.match(/score (cp|mate) (-?\d+)/);
const pvMatch = msg.match(/multipv (\d+)/);
const depthMatch = msg.match(/depth (\d+)/);
const moveMatch = msg.match(/ pv (\w+)/);
if (scoreMatch && pvMatch && moveMatch) {
const type = scoreMatch[1];
let value = parseInt(scoreMatch[2]);
const mpv = parseInt(pvMatch[1]);
const depth = parseInt(depthMatch?.[1] || 0);
if (type === 'cp') value = value / 100;
// Store ALL candidate moves from MultiPV
State.candidates[mpv] = {
move: moveMatch[1],
eval: { type, value },
depth,
};
// PV1 is always the best move
if (mpv === 1) {
State.currentEval = { type, value, depth };
State.currentBestMove = moveMatch[1];
}
// Extract opponent's expected response from PV1 line
if (mpv === 1 && msg.includes(' pv ')) {
const pvMoves = msg.split(' pv ')[1].split(' ');
if (pvMoves.length > 1) {
State.opponentResponse = pvMoves[1];
}
}
}
},
handleBestMove: async (bestMove, isBook = false) => {
State.isThinking = false;
State.moveCount++;
const fen = State.lastFen;
const phase = HumanStrategy.getGamePhase(fen);
// Use HumanStrategy to decide which move to actually play
const moveResult = HumanStrategy.selectMove(fen, bestMove, isBook);
const finalMove = moveResult.move;
Utils.log(`Move #${State.moveCount} [${phase}]: ${finalMove} (${moveResult.reason})${moveResult.isBest ? '' : ' [SUBOPTIMAL]'}`);
// Draw best move arrow (green) and chosen move if different (yellow)
UI.drawMove(bestMove, '#4caf50');
if (finalMove !== bestMove) {
UI.drawMove(finalMove, '#ffcc00', true);
}
if (CONFIG.showThreats && State.opponentResponse) {
UI.drawMove(State.opponentResponse, '#ff5252', true);
}
UI.updatePanel(State.currentEval, { move: `${finalMove}${moveResult.isBest ? '' : '*'}` });
// Track for correlation management
HumanStrategy.trackMove(moveResult.isBest);
if (CONFIG.auto.enabled && Game.isMyTurn(State.lastFen)) {
const delay = HumanStrategy.calculateDelay(fen, moveResult, isBook);
Utils.log(`Waiting ${Math.round(delay)}ms (${moveResult.reason})...`);
await Utils.sleep(delay);
await Humanizer.executeMove(finalMove);
}
}
};
const Humanizer = {
createEvent: (type, x, y, options = {}) => {
const defaults = {
bubbles: true,
cancelable: true,
view: window,
detail: 1,
screenX: x,
screenY: y,
clientX: x,
clientY: y,
pointerId: 1,
pointerType: 'mouse',
isPrimary: true,
button: 0,
buttons: 1,
which: 1,
composed: true
};
return new PointerEvent(type, { ...defaults, ...options });
},
getElementsAt: (x, y) => {
return document.elementsFromPoint(x, y).filter(el =>
el.tagName !== 'HTML' && el.tagName !== 'BODY' && !el.classList.contains('ba-overlay')
);
},
makeGodMove: (from, to, promo) => false,
showClick: (x, y, color = 'red') => {
const dot = document.createElement('div');
dot.style.cssText = `
position: absolute;
left: ${x}px; top: ${y}px;
width: 12px; height: 12px;
background: ${color}; border-radius: 50%;
z-index: 100000; pointer-events: none;
transform: translate(-50%, -50%);
box-shadow: 0 0 4px white;
`;
document.body.appendChild(dot);
setTimeout(() => dot.remove(), 800);
},
dragDrop: async (fromSq, toSq) => {
const board = Game.getBoard();
if (!board) return;
const startPos = Humanizer.getCoords(fromSq);
const endPos = Humanizer.getCoords(toSq);
if (!startPos || !endPos) return;
Humanizer.showClick(startPos.x, startPos.y, '#00ff00');
const fromCoords = Game.squareToCoords(fromSq);
const pieceEl = board.querySelector(`.piece.square-${fromCoords}`) ||
document.elementFromPoint(startPos.x, startPos.y);
const targetSource = pieceEl || board;
const opts = { bubbles: true, composed: true, buttons: 1, pointerId: 1, isPrimary: true };
// Slight random offset on pickup (humans don't click exact center)
const pickupNoise = () => Utils.gaussianRandom(0, 2);
const sx = startPos.x + pickupNoise();
const sy = startPos.y + pickupNoise();
targetSource.dispatchEvent(new PointerEvent('pointerover', { ...opts, clientX: sx, clientY: sy }));
targetSource.dispatchEvent(new PointerEvent('pointerdown', { ...opts, clientX: sx, clientY: sy }));
targetSource.dispatchEvent(new MouseEvent('mousedown', { ...opts, clientX: sx, clientY: sy }));
// Human pickup hesitation: variable delay
await Utils.sleep(Utils.randomRange(30, 90));
// Generate Bézier control point for curved path (humans don't drag in straight lines)
const dx = endPos.x - startPos.x;
const dy = endPos.y - startPos.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const curvature = Utils.gaussianRandom(0, dist * 0.15);
const cpX = (startPos.x + endPos.x) / 2 + curvature * (Math.random() > 0.5 ? 1 : -1);
const cpY = (startPos.y + endPos.y) / 2 + curvature * (Math.random() > 0.5 ? 1 : -1);
// Variable step count based on distance (more steps = smoother = more realistic)
const steps = Math.max(8, Math.min(18, Math.round(dist / 8) + Math.round(Math.random() * 4)));
for (let i = 1; i <= steps; i++) {
const t = i / steps;
// Quadratic Bézier: B(t) = (1-t)^2*P0 + 2*(1-t)*t*CP + t^2*P1
const oneMinusT = 1 - t;
let curX = oneMinusT * oneMinusT * startPos.x + 2 * oneMinusT * t * cpX + t * t * endPos.x;
let curY = oneMinusT * oneMinusT * startPos.y + 2 * oneMinusT * t * cpY + t * t * endPos.y;
// Add per-step noise (decreasing as we approach target)
const noiseScale = Math.max(0.5, (1 - t) * 3);
curX += Utils.gaussianRandom(0, noiseScale);
curY += Utils.gaussianRandom(0, noiseScale);
// Variable pressure (humans vary grip)
const pressure = 0.4 + Math.random() * 0.3;
document.dispatchEvent(new PointerEvent('pointermove', {
...opts, clientX: curX, clientY: curY, pressure
}));
document.dispatchEvent(new MouseEvent('mousemove', { ...opts, clientX: curX, clientY: curY }));
// Variable inter-step delay (humans accelerate mid-drag, slow at ends)
const speedCurve = Math.sin(t * Math.PI); // slow-fast-slow
const stepDelay = Math.max(2, Math.round(Utils.randomRange(4, 14) * (1.2 - speedCurve * 0.8)));
if (Math.random() < 0.4) await Utils.sleep(stepDelay);
}
// Slight overshoot then correction (very human)
if (Math.random() < 0.3) {
const overshootX = endPos.x + Utils.gaussianRandom(0, 4);
const overshootY = endPos.y + Utils.gaussianRandom(0, 4);
document.dispatchEvent(new PointerEvent('pointermove', { ...opts, clientX: overshootX, clientY: overshootY }));
await Utils.sleep(Utils.randomRange(8, 20));
document.dispatchEvent(new PointerEvent('pointermove', { ...opts, clientX: endPos.x, clientY: endPos.y }));
await Utils.sleep(Utils.randomRange(5, 12));
}
const toCoords = Game.squareToCoords(toSq);
const targetEl = board.querySelector(`.square-${toCoords}`) ||
document.elementFromPoint(endPos.x, endPos.y);
const dropTarget = targetEl || board;
// Drop with slight noise
const dropX = endPos.x + Utils.gaussianRandom(0, 1.5);
const dropY = endPos.y + Utils.gaussianRandom(0, 1.5);
Humanizer.showClick(endPos.x, endPos.y, 'red');
dropTarget.dispatchEvent(new PointerEvent('pointerup', { ...opts, clientX: dropX, clientY: dropY }));
dropTarget.dispatchEvent(new MouseEvent('mouseup', { ...opts, clientX: dropX, clientY: dropY }));
dropTarget.dispatchEvent(new PointerEvent('click', { ...opts, clientX: dropX, clientY: dropY }));
},
clickSquare: async (sq) => { },
executeMove: async (move) => {
const currentFen = Game.getFen();
if (currentFen !== State.lastFen && State.moveCount > 0) return;
const from = move.substring(0, 2);
const to = move.substring(2, 4);
const promo = move.length > 4 ? move[4] : 'q';
// Detect if this is a promotion move (pawn reaching rank 1 or 8)
const isPromotion = (to[1] === '8' || to[1] === '1') && Humanizer.isPawnMove(from);
if (Humanizer.makeGodMove(from, to, isPromotion ? promo : null)) {
Utils.log('API Success?');
}
Utils.log(`Auto-playing (Drag): ${from} -> ${to}${isPromotion ? '=' + promo : ''}`);
await Humanizer.dragDrop(from, to);
if (isPromotion) {
await Humanizer.handlePromotion(promo);
}
},
isPawnMove: (fromSq) => {
const board = Game.getBoard();
if (!board) return false;
const coords = Game.squareToCoords(fromSq);
const piece = board.querySelector(`.piece.square-${coords}`);
if (piece) {
const classes = piece.className;
return classes.includes('wp') || classes.includes('bp');
}
// Fallback: if piece on rank 2 or 7, likely a pawn
return fromSq[1] === '2' || fromSq[1] === '7';
},
handlePromotion: async (promo = 'q') => {
const pieceMap = { q: 'queen', r: 'rook', b: 'bishop', n: 'knight' };
const pieceName = pieceMap[promo] || 'queen';
Utils.log(`Promotion: selecting ${pieceName}`);
// Poll for the promotion dialog to appear (up to 2 seconds)
let promoEl = null;
for (let i = 0; i < 20; i++) {
await Utils.sleep(100);
// Try multiple selector strategies for Chess.com's promotion UI
const selectors = [
`.promotion-piece[data-piece="${promo}"]`,
`.promotion-piece.w${promo}, .promotion-piece.b${promo}`,
`.promotion-window .promotion-piece:first-child`,
`[class*="promotion"] [class*="${pieceName}"]`,
`[class*="promotion"] [class*="${promo}"]`,
`.promotion-area .promotion-piece:first-child`,
];
for (const sel of selectors) {
promoEl = document.querySelector(sel);
if (promoEl) break;
}
// Also try: find any promotion container and click the first piece (queen is always first)
if (!promoEl) {
const promoContainer = document.querySelector('.promotion-window, .promotion-area, [class*="promotion-"]');
if (promoContainer) {
const pieces = promoContainer.querySelectorAll('.promotion-piece, [class*="piece"]');
if (pieces.length > 0) {
// Queen is always the first option in Chess.com's promotion dialog
promoEl = promo === 'q' ? pieces[0] : pieces[{ r: 1, b: 2, n: 3 }[promo] || 0];
}
}
}
if (promoEl) break;
}
if (promoEl) {
const rect = promoEl.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
const opts = { bubbles: true, composed: true, buttons: 1, pointerId: 1, isPrimary: true };
promoEl.dispatchEvent(new PointerEvent('pointerdown', { ...opts, clientX: x, clientY: y }));
await Utils.sleep(30);
promoEl.dispatchEvent(new PointerEvent('pointerup', { ...opts, clientX: x, clientY: y }));
promoEl.dispatchEvent(new MouseEvent('mouseup', { ...opts, clientX: x, clientY: y }));
promoEl.dispatchEvent(new PointerEvent('click', { ...opts, clientX: x, clientY: y }));
promoEl.click();
Utils.log(`Promotion: clicked ${pieceName}`);
} else {
Utils.log('Promotion: dialog not found, trying direct click at queen position', 'warn');
// Last resort: click at the expected queen position on the board
const board = Game.getBoard();
if (board) {
const boardRect = board.getBoundingClientRect();
const sqSize = boardRect.width / 8;
// Queen promotion square is at the top of the promotion popup
const promoX = boardRect.left + sqSize / 2;
const promoY = boardRect.top + sqSize / 2;
const opts = { bubbles: true, composed: true, buttons: 1 };
document.elementFromPoint(promoX, promoY)?.click();
}
}
},
getCoords: (sq) => {
const board = Game.getBoard();
if (!board) return null;
const rect = board.getBoundingClientRect();
const sqSize = rect.width / 8;
const isFlipped = State.playerColor === 'b';
const f = sq.charCodeAt(0) - 97;
const r = parseInt(sq[1]) - 1;
const x = rect.left + (isFlipped ? 7 - f : f) * sqSize + sqSize / 2;
const y = rect.top + (isFlipped ? r : 7 - r) * sqSize + sqSize / 2;
return { x, y };
}
};
const Main = {
_queueing: false,
init: async () => {
UI.injectStyles();
UI.createInterface();
let board = null;
while (!board) {
board = Game.getBoard();
if (!board) await Utils.sleep(500);
}
Utils.log('Board detected. Starting Engine...');
await Engine.init();
Main.setupObservers();
Main.gameLoop();
document.addEventListener('keydown', (e) => {
if (e.key === 'a' && !e.ctrlKey && !e.shiftKey && !e.target.matches('input')) {
CONFIG.auto.enabled = !CONFIG.auto.enabled;
Utils.log(`Auto-Play: ${CONFIG.auto.enabled}`);
UI.updatePanel(State.currentEval, {});
}
if (e.key === 'x' && !e.target.matches('input')) {
UI.toggleStealth();
}
});
},
checkAutoQueue: async () => {
if (!CONFIG.auto.autoQueue || !CONFIG.auto.enabled) return;
if (Main._queueing) return;
Main._queueing = true;
// Session management: take breaks to avoid pattern detection
CONFIG.session.gamesPlayed++;
if (CONFIG.session.gamesPlayed >= CONFIG.session.maxGamesPerSession) {
const breakMs = CONFIG.session.breakDurationMs + Utils.randomRange(0, 60000);
Utils.log(`Session break: ${Math.round(breakMs / 1000)}s after ${CONFIG.session.gamesPlayed} games`, 'warn');
await Utils.sleep(breakMs);
CONFIG.session.gamesPlayed = 0;
CONFIG.session.currentWinStreak = 0;
}
const selectors = [
'button[data-cy="game-over-modal-new-game-button"]',
'button[data-cy="game-over-modal-rematch-button"]',
'button[data-cy="new-game-index-main"]',
'.game-over-buttons-button',
'.game-over-button-component.primary',
'.ui_v5-button-component.ui_v5-button-primary',
];
for (let attempt = 0; attempt < 5; attempt++) {
for (const sel of selectors) {
const btn = document.querySelector(sel);
if (btn && btn.offsetParent !== null) {
if (sel.includes('rematch') && document.querySelector(selectors[0])) continue;
const delay = Utils.humanDelay(2000, 6000);
Utils.log(`Auto-Queue: Clicking "${btn.textContent.trim()}" in ${Math.round(delay)}ms (game #${CONFIG.session.gamesPlayed})...`);
await Utils.sleep(delay);
btn.click();
Main._queueing = false;
return;
}
}
await Utils.sleep(1000);
}
Utils.log('Auto-Queue: No new game button found', 'warn');
Main._queueing = false;
},
setupObservers: () => {
const movesList = Utils.query(SELECTORS.moves) || document.body;
const observer = new MutationObserver(() => {
requestAnimationFrame(Main.gameLoop);
});
observer.observe(movesList, { childList: true, subtree: true, characterData: true });
const gameOverObserver = new MutationObserver(() => {
if (Utils.query(SELECTORS.gameOver) && CONFIG.auto.enabled && CONFIG.auto.autoQueue) {
Main.checkAutoQueue();
}
});
gameOverObserver.observe(document.body, { childList: true, subtree: true });
setInterval(Main.gameLoop, CONFIG.pollInterval);
setInterval(() => {
if (Utils.query(SELECTORS.gameOver) && CONFIG.auto.enabled && CONFIG.auto.autoQueue) {
Main.checkAutoQueue();
}
}, 3000);
},
gameLoop: () => {
const fen = Game.getFen();
if (!fen || fen === State.lastFen) return;
State.playerColor = Game.detectColor();
const fenParts = fen.split(' ');
const isStartPos = fenParts[0] === 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR';
const fenMoveNum = parseInt(fenParts[5] || '1');
if (isStartPos || (fenMoveNum <= 1 && State.moveCount > 3)) {
Utils.log('New game detected - resetting humanization', 'warn');
HumanStrategy.resetGame();
Main._queueing = false;
}
State.lastFen = fen;
State.currentEval = null;
State.candidates = {};
UI.clearOverlay();
UI.updatePanel(null, null);
if (Utils.query(SELECTORS.gameOver)) {
if (CONFIG.auto.autoQueue) {
setTimeout(Main.checkAutoQueue, 2000);
}
return;
}
if (Game.isMyTurn(fen)) {
Engine.analyze(fen);
} else {
UI.updateStatus('#888');
}
}
};
Main.init();
})();