Chess.com cheat engine — v15.1 (account-level warmup, winrate targeting, hard repertoire, tilt, smart premove gating, TC lock, engine rotation, messy resign, hardware persona)
// ==UserScript==
// @name Chess.com Cheat Engine
// @namespace http://tampermonkey.net/
// @version 15.1
// @description Chess.com cheat engine — v15.1 (account-level warmup, winrate targeting, hard repertoire, tilt, smart premove gating, TC lock, engine rotation, messy resign, hardware persona)
// @author rexxx
// @license MIT
// @match https://www.chess.com/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @connect unpkg.com
// @connect cdn.jsdelivr.net
// @connect cdnjs.cloudflare.com
// @connect lichess.org
// @connect explorer.lichess.ovh
// @connect stockfish.online
// @connect chess-api.com
// @connect tablebase.lichess.ovh
// @run-at document-idle
// @grant unsafeWindow
// ==/UserScript==
(function () {
'use strict';
// ═══════════════════════════════════════════
// ANTI-DETECTION: MINIMAL TAB-INACTIVITY GUARD
// ═══════════════════════════════════════════
// Philosophy: every prototype override is itself a fingerprint.
// - document.hidden lying while rAF still slows in the background = impossible state
// - patched addEventListener.toString = '[native code]' while behavior differs = honeypot
// - RAF timestamp patching produces impossibly-uniform 60fps gaps under throttling
// So we stop all of that and just swallow `visibilitychange` so the page doesn't
// proactively pause its own timers/engine when the tab goes to the background.
// The browser's own throttling still happens, which matches what a real user would show.
try {
const swallow = (e) => { e.stopImmediatePropagation(); e.stopPropagation(); };
document.addEventListener('visibilitychange', swallow, true);
document.addEventListener('webkitvisibilitychange', swallow, true);
document.addEventListener('mozvisibilitychange', swallow, true);
document.addEventListener('msvisibilitychange', swallow, true);
} catch (e) { /* silent */ }
// ═══════════════════════════════════════════
// SETTINGS PERSISTENCE
// ═══════════════════════════════════════════
const Settings = {
_defaults: null,
_key: 'ba_config_v13e',
init(defaults) {
Settings._defaults = JSON.parse(JSON.stringify(defaults));
const saved = GM_getValue(Settings._key, null);
if (saved) {
try {
const parsed = JSON.parse(saved);
Settings._deepMerge(defaults, parsed);
Utils.log('Settings loaded from storage', 'info');
} catch (e) {
Utils.log('Failed to load settings, using defaults', 'warn');
}
}
},
save(config) {
try {
// Only save user-configurable fields
const toSave = {
engineType: config.engineType,
targetRating: config.targetRating,
playStyle: config.playStyle,
engineDepth: config.engineDepth,
multiPV: config.multiPV,
humanization: config.humanization,
timing: config.timing,
auto: config.auto,
dragSpeed: config.dragSpeed,
arrowOpacity: config.arrowOpacity,
showThreats: config.showThreats,
useBook: config.useBook,
useTablebase: config.useTablebase,
session: config.session,
autoLose: config.autoLose,
autoResign: config.autoResign,
opponentAdaptation: config.opponentAdaptation,
timePressure: config.timePressure,
antiDetection: config.antiDetection,
// v15.1 additions
account: config.account,
warmup: config.warmup,
winrateTarget: config.winrateTarget,
repertoireHard: config.repertoireHard,
tilt: config.tilt,
premoveGating: config.premoveGating,
tcLock: config.tcLock,
engineRotation: config.engineRotation,
messyResign: config.messyResign,
hardwarePersona: config.hardwarePersona,
};
GM_setValue(Settings._key, JSON.stringify(toSave));
} catch (e) { /* silent fail */ }
},
reset(config) {
Settings._deepMerge(config, Settings._defaults);
GM_setValue(Settings._key, '{}');
},
_deepMerge(target, source) {
for (const key of Object.keys(source)) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])
&& target[key] && typeof target[key] === 'object') {
Settings._deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
};
// ═══════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════
const CONFIG = {
// Engine source: 'api' (chess-api.com SF18), 'stockfish_online', 'local' (SF10 worker)
engineType: 'api',
// Target rating (800-2800) - auto-tunes all humanization params
targetRating: 1800,
// Play style: 'universal', 'aggressive', 'positional', 'endgame_specialist'
playStyle: 'universal',
// Engine depth settings
engineDepth: {
base: 14,
min: 6,
max: 20,
dynamicDepth: true,
},
multiPV: 5,
pollInterval: 1000,
// API engine config
api: {
chessApi: {
url: 'https://chess-api.com/v1',
maxThinkingTime: 100, // ms server-side limit
timeout: 5000,
},
stockfishOnline: {
url: 'https://stockfish.online/api/s/v2.php',
timeout: 8000,
},
},
// Syzygy tablebase for perfect endgame (<=7 pieces)
useTablebase: true,
tablebase: {
url: 'https://tablebase.lichess.ovh/standard',
maxPieces: 7,
},
// --- HUMANIZATION CORE ---
humanization: {
enabled: true,
targetEngineCorrelation: 0.62,
suboptimalMoveRate: {
opening: 0.15,
middlegame: 0.22,
endgame: 0.18,
},
winningDegradation: {
enabled: true,
tiers: [
{ evalAbove: 2.0, extraSuboptimalRate: 0.03 },
{ evalAbove: 4.0, extraSuboptimalRate: 0.06 },
{ evalAbove: 6.0, extraSuboptimalRate: 0.09 },
{ evalAbove: 8.0, extraSuboptimalRate: 0.12 },
],
},
losingSharpness: {
enabled: true,
evalBelow: -0.8,
suboptimalReduction: 0.40,
},
maxAcceptableCPLoss: {
opening: 55,
middlegame: 110,
endgame: 65,
},
blunder: {
chance: 0.02,
onlyInComplexPositions: true,
maxCPLoss: 200,
disableWhenEvalBetween: [-2.0, 2.0],
},
streaks: {
enabled: true,
perfectStreakMax: 7,
sloppyStreakMax: 3,
},
personalityVariance: {
enabled: true,
suboptimalRateJitter: 0.15,
depthJitter: 3,
timingJitter: 0.40,
},
// Accuracy clustering: humans have "hot streaks" and "cold streaks"
accuracyClustering: {
enabled: true,
hotStreakChance: 0.15, // chance to enter "focused" mode (fewer errors)
coldStreakChance: 0.07, // chance to enter "tired" mode (more errors)
streakDuration: { min: 3, max: 8 }, // how many moves the streak lasts
},
// Opening repertoire: play consistent openings per color
repertoireConsistency: {
enabled: true,
},
// --- ANTI-CORRELATION POISONING ---
antiCorrelation: {
enabled: true,
// Hard cap: never play SF#1 more than this % of the time.
// Overridden per-rating by RatingProfile.apply().
maxTopMoveRate: 0.55,
// When SF#2/3 are within this eval of SF#1, prefer them sometimes.
// Wider threshold (was 30cp) catches more "essentially equal" positions.
closeEvalThreshold: 0.40,
// Higher prefer rate (was 20%) so we diversify more aggressively.
closeEvalPreferRate: 0.28,
// Miss easy tactics: if a tactic gains < this, sometimes play positional instead
missSmallTacticThreshold: 1.5,
missSmallTacticRate: 0.08, // up from 5% — humans miss small tactics often
},
// --- CONSISTENT WEAKNESS PROFILE ---
weaknessProfile: {
enabled: true,
// Persisted per-account seed — generates deterministic weaknesses
// (auto-generated on first run, saved with settings)
seed: null,
},
// --- LICHESS PLAYER MOVE DATABASE ---
playerMoveDB: {
enabled: true,
// Query Lichess player games at this rating range to find "real human" moves
ratingWindow: 200, // +/- from target rating
minGames: 5, // minimum games a move needs to be considered
preferRate: 0.15, // 15% chance to use a player DB move instead of engine move
timeout: 3000,
},
// --- THINK TIME ↔ ACCURACY COUPLING ---
timingAccuracyCoupling: {
enabled: true,
// When playing the best move after thinking long: natural (humans figure it out)
// When playing fast: lower accuracy (pattern recognition, sometimes wrong)
fastMoveSuboptimalBoost: 0.10, // +10% suboptimal rate for fast moves
slowMoveBestBoost: 0.25, // +25% best move rate for slow thinks
// Noise: occasional "fast good move" and "slow bad move"
noiseRate: 0.12, // 12% of moves invert the coupling
},
},
// --- TIMING / HUMANIZER ---
// Drag speed: controls how fast pieces physically move across the board
// 0.3 = very fast, 1.0 = normal/human, 2.0 = slow/deliberate
dragSpeed: 1.0,
timing: {
base: { min: 1500, max: 5000 },
forced: { min: 500, max: 1500 },
book: { min: 600, max: 1800 },
// First few moves of the game (humans play these fast from memory)
earlyGame: { min: 400, max: 1500 },
complex: { min: 3500, max: 9000 },
simple: { min: 1000, max: 2800 },
longThink: { chance: 0.10, min: 6000, max: 15000 },
instantMove: { chance: 0.03, min: 300, max: 700 },
fatigue: {
enabled: true,
startMove: 20,
msPerMove: 25,
cap: 1200,
},
// Clock awareness: play faster when low on time
clockAware: {
enabled: true,
// Below these thresholds, multiply timing by the factor
thresholds: [
{ secondsBelow: 30, timingMult: 0.25 }, // time scramble
{ secondsBelow: 60, timingMult: 0.40 },
{ secondsBelow: 120, timingMult: 0.60 },
{ secondsBelow: 300, timingMult: 0.80 },
],
},
// Sequence pattern variation: avoid uniform timing across moves
sequenceVariation: {
enabled: true,
// If last N moves were all in similar time range, force variety
windowSize: 4,
similarityThreshold: 0.30, // if std_dev / mean < this, force outlier
},
// Premove simulation: sometimes play instantly after opponent's move
premove: {
enabled: true,
chance: 0.08, // chance to "premove" when we predicted opponent's reply
delay: { min: 50, max: 250 },
},
},
// --- ANTI-DETECTION HARDENING ---
antiDetection: {
// Maximum games per hour (Chess.com flags high game throughput)
maxGamesPerHour: 6,
// Random AFK delays: sometimes pause before making a move (simulates distraction)
randomAFK: {
enabled: true,
chance: 0.04, // 4% chance per move to go "AFK"
delay: { min: 4000, max: 15000 }, // 4-15 seconds of doing nothing
},
// Occasional non-engine move: pick a random legal move that ISN'T in the PV list
// This breaks engine correlation in a way that's undetectable
randomLegalMoveChance: 0.01, // 1% chance to play a completely random legal move
randomLegalMaxCPLoss: 250, // don't play random moves that lose more than this
// Change-of-mind fake-out: drag toward a wrong square, hesitate, then redirect to the real target
changeOfMind: {
enabled: true,
chance: 0.12, // 12% of moves will fake-out
hesitateMs: { min: 120, max: 350 }, // pause at the fake square before redirecting
},
// Session jitter: randomize session length so it's not always exactly maxGamesPerSession
sessionLengthJitter: 0.30, // +/- 30% variation on maxGamesPerSession
// Minimum break between games (additional random delay after auto-queue clicks)
minBreakBetweenGames: { min: 3000, max: 12000 },
// Telemetry noise generation while waiting
telemetryNoise: {
enabled: true,
hoverChance: 0.05, // Chance to fake hover a piece
premoveCancelChance: 0.02, // Chance to queue and cancel a fake premove
uiClickChance: 0.03, // Chance to click harmless UI elements (chat, tabs)
},
},
// --- SESSION MANAGEMENT ---
session: {
maxGamesPerSession: 8,
breakDurationMs: 300000,
maxWinStreak: 6,
gamesPlayed: 0,
wins: 0,
currentWinStreak: 0,
// Tracking for rate limiter
gameTimestamps: [],
},
// --- AUTO-LOSE MODE ---
autoLose: {
enabled: true,
// When win streak hits this, switch to intentional-lose mode
triggerStreak: 5,
// How badly to play: suboptimal rate, blunder chance, max CP loss allowed
suboptimalRate: 0.80,
blunderChance: 0.25,
maxCPLoss: 400,
// Target correlation when losing (very low = obviously bad)
targetCorrelation: 0.20,
// Don't resign too fast — play naturally bad, lose by checkmate/time
minMovesBeforeLosing: 15,
},
// --- AUTO-RESIGN ---
autoResign: {
enabled: true,
// Resign when eval is below this for N consecutive moves
evalThreshold: -5.0,
consecutiveMoves: 3,
// Probability to resign (humans sometimes play on even in lost positions)
resignChance: 0.70,
// Never resign before this move number
minMoveNumber: 10,
// Random delay before resigning (looks human)
delay: { min: 2000, max: 8000 },
},
// --- OPPONENT STRENGTH ADAPTATION ---
opponentAdaptation: {
enabled: true,
// How many rating points above opponent to target
ratingEdge: 400,
// Clamp the adjusted rating to these bounds
minRating: 800,
maxRating: 2800,
// Don't re-adapt mid-game, only at game start
},
// --- TIME PRESSURE ACCURACY DROP ---
timePressure: {
enabled: true,
// Below these clock thresholds, multiply suboptimal rate and blunder chance
thresholds: [
{ secondsBelow: 15, suboptimalMult: 3.0, blunderMult: 5.0, maxCPLossMult: 2.5 },
{ secondsBelow: 30, suboptimalMult: 2.2, blunderMult: 3.0, maxCPLossMult: 2.0 },
{ secondsBelow: 60, suboptimalMult: 1.6, blunderMult: 2.0, maxCPLossMult: 1.5 },
{ secondsBelow: 120, suboptimalMult: 1.2, blunderMult: 1.3, maxCPLossMult: 1.2 },
],
},
auto: {
enabled: false,
autoQueue: true,
},
arrowOpacity: 0.8,
showThreats: true,
stealthMode: false,
useBook: true,
// --- ACCOUNT-LEVEL STATE (per-Tampermonkey-install, persisted) ---
// Treats each install as one "account". If you reset Tampermonkey
// storage / use a different browser profile, this resets too.
account: {
// Total games played by this script ever (used by warmup ramp).
totalGamesPlayed: 0,
// Per-account opening repertoire — chosen on first warmup game,
// then locked. {white: ['e4'], black: {e4: 'c5', d4: 'Nf6', other: null}}
repertoire: { white: null, black: null },
// Hardware persona: 'mouse' | 'trackpad' | 'tablet'.
hardware: null,
// Recent results, newest last. Used by winrate targeting + tilt.
// Each entry: 'W' | 'L' | 'D'.
recentResults: [],
// First seen TC of the current session (locked once set).
sessionTC: null,
// Selected engine source for the CURRENT game (re-rolled each game).
currentEngine: null,
},
// --- WARMUP MODE (new-account ramp) ---
warmup: {
enabled: true, // auto-detect via totalGamesPlayed
manualOverride: false, // force ON regardless of game count
durationGames: 20, // games until full target rating
startEloOffset: -350, // begin this far below user target
shape: 'sigmoid', // 'linear' | 'sigmoid' (sigmoid is more human-like)
},
// --- WINRATE TARGETING ---
winrateTarget: {
enabled: true,
target: 0.52, // aim for ~52% (real player range 45-58%)
sampleSize: 12, // window of recent games to compute rolling winrate
overshootBoost: 0.50, // each +10% above target adds this much to lose-chance
},
// --- HARD REPERTOIRE ENFORCEMENT ---
// Different from `humanization.repertoireConsistency` (which is per-game soft).
// This is per-account hard: pick 1-2 first moves per color and stick to them.
repertoireHard: {
enabled: true,
whiteFirstMoves: ['e4', 'd4', 'c4', 'Nf3'], // candidate set
blackVsE4: ['c5', 'e5', 'e6', 'c6'],
blackVsD4: ['Nf6', 'd5', 'e6'],
blackVsOther: ['Nf6', 'd5'],
picksPerColor: 2, // pick 1-2 per color from candidates
deviationRate: 0.05, // 5% chance to play out of repertoire
},
// --- TILT MECHANIC ---
tilt: {
enabled: true,
triggerOn: ['L'], // event types that cause tilt ('L' loss, optionally 'D')
duration: { min: 2, max: 4 }, // games of degraded play after trigger
suboptimalBoost: 0.18, // additive to suboptimal rate
blunderMult: 1.6, // multiplier on blunder chance
timingMult: 1.25, // 25% slower (frustrated overthinking)
},
// --- SMART PREMOVE GATING ---
// Old behavior: premove on any predicted reply.
// New: only premove when the reply is forced (single legal move) or an
// obvious recapture, since real players premove these specifically.
premoveGating: {
enabled: true,
forcedOnly: true, // require single legal reply
allowRecaptures: true, // also allow obvious recaptures
},
// --- TIME-CONTROL LOCK ---
// If enabled, the FIRST TC seen in a session locks the queue. We refuse
// to auto-queue games of a different TC (real players stick to one TC).
tcLock: {
enabled: true,
},
// --- ENGINE SOURCE ROTATION ---
// Per-game randomization between sources prevents tactical signature
// from being identical across games. Weights normalized.
engineRotation: {
enabled: true,
weights: { api: 0.55, local_full: 0.30, local_shallow: 0.15 },
shallowDepth: 11, // depth used when 'local_shallow' picked
},
// --- MESSY RESIGNATION ---
// Replace the surgical "resign at -5.0 immediately" pattern with
// human-style behavior: hesitate, sometimes blunder once first,
// sometimes hold completely losing positions for a while.
messyResign: {
enabled: true,
blunderBeforeChance: 0.22, // chance to play one bad move then resign
holdLostChance: 0.18, // chance to refuse to resign even past threshold
extraMovesBeforeResign: { min: 0, max: 3 }, // play 0-3 more moves after threshold met
},
// --- HARDWARE PERSONA ---
// Different input devices produce different mouse fingerprints. Choose
// one per account at first run, then keep it stable.
hardwarePersona: {
enabled: true,
// Multipliers applied per-persona:
// jitterScale: how noisy the bezier path is (trackpad > mouse > tablet)
// clickHoldMs: how long mousedown before mouseup
// speedScale: overall drag-speed multiplier
personas: {
mouse: { jitterScale: 1.00, clickHoldMs: { min: 50, max: 110 }, speedScale: 1.00 },
trackpad: { jitterScale: 1.45, clickHoldMs: { min: 70, max: 150 }, speedScale: 0.85 },
tablet: { jitterScale: 1.20, clickHoldMs: { min: 90, max: 180 }, speedScale: 0.95 },
},
},
};
// ═══════════════════════════════════════════
// ACCOUNT-LEVEL POLICY MODULE
// ═══════════════════════════════════════════
// Owns "between-games" decisions: warmup ramp, winrate targeting, hard
// repertoire, tilt, hardware persona, engine rotation, TC lock.
// Called once per game from Main.gameLoop on new-game detection.
const Account = {
// ---------- WARMUP ----------
isInWarmup: () => {
const w = CONFIG.warmup;
if (!w.enabled && !w.manualOverride) return false;
if (w.manualOverride) return true;
return CONFIG.account.totalGamesPlayed < w.durationGames;
},
// Returns the effective target rating for THIS game (warmup-adjusted).
effectiveTargetRating: () => {
const baseTarget = CONFIG.targetRating;
if (!Account.isInWarmup()) return baseTarget;
const w = CONFIG.warmup;
const n = CONFIG.account.totalGamesPlayed;
// Progress 0..1 across the warmup window
const p = Math.min(1, Math.max(0, n / Math.max(1, w.durationGames)));
// Sigmoid: slow ramp at start, accelerate, then plateau (more human)
const easeFn = (p) => {
if (w.shape === 'linear') return p;
// Smoothstep-like sigmoid centered on 0.5
return p * p * (3 - 2 * p);
};
const offset = w.startEloOffset * (1 - easeFn(p));
return Math.max(800, Math.round(baseTarget + offset));
},
// ---------- WINRATE TARGETING ----------
recentWinrate: () => {
const wt = CONFIG.winrateTarget;
const r = CONFIG.account.recentResults || [];
if (r.length === 0) return null;
const window = r.slice(-wt.sampleSize);
const wins = window.filter(x => x === 'W').length;
const games = window.length;
return games > 0 ? wins / games : null;
},
// Returns extra lose-chance to apply on top of streak-based logic.
// 0 = no boost; 1 = guaranteed lose this game.
winrateOvershootBoost: () => {
const wt = CONFIG.winrateTarget;
if (!wt.enabled) return 0;
const wr = Account.recentWinrate();
if (wr == null) return 0;
const overshoot = wr - wt.target;
if (overshoot <= 0.05) return 0; // within tolerance
// Each +10% above target adds wt.overshootBoost worth of lose probability
return Math.min(0.85, (overshoot / 0.10) * wt.overshootBoost);
},
recordResult: (result) => {
// result: 'W' | 'L' | 'D'
if (!result || !'WLD'.includes(result)) return;
CONFIG.account.recentResults = (CONFIG.account.recentResults || []).slice(-50);
CONFIG.account.recentResults.push(result);
CONFIG.account.totalGamesPlayed = (CONFIG.account.totalGamesPlayed || 0) + 1;
Settings.save(CONFIG);
},
// ---------- TILT ----------
// _tiltGamesLeft is in-memory state; we don't persist it (sessions reset OK).
_tiltGamesLeft: 0,
maybeStartTilt: (lastResult) => {
const t = CONFIG.tilt;
if (!t.enabled) return;
if (!t.triggerOn.includes(lastResult)) return;
const games = Math.round(Utils.randomRange(t.duration.min, t.duration.max));
Account._tiltGamesLeft = games;
Utils.log(`Tilt: triggered by ${lastResult}, ${games} games of degraded play`, 'warn');
UI.toast('Tilt', `${lastResult === 'L' ? 'Frustrated' : 'Off-balance'} after last game — playing worse for ${games} games`, 'warn', 5000);
},
consumeTiltTick: () => {
// Called once per new game. Returns the active tilt config or null.
if (Account._tiltGamesLeft <= 0) return null;
Account._tiltGamesLeft--;
return CONFIG.tilt;
},
// ---------- HARDWARE PERSONA ----------
ensureHardwarePersona: () => {
if (!CONFIG.hardwarePersona.enabled) return null;
if (!CONFIG.account.hardware) {
const choices = ['mouse', 'trackpad', 'tablet'];
// Bias toward mouse since most desktop chess players use one
const weights = [0.62, 0.30, 0.08];
let r = Math.random(), acc = 0;
for (let i = 0; i < choices.length; i++) {
acc += weights[i];
if (r < acc) { CONFIG.account.hardware = choices[i]; break; }
}
Utils.log(`Hardware persona chosen for this account: ${CONFIG.account.hardware}`, 'info');
Settings.save(CONFIG);
}
return CONFIG.hardwarePersona.personas[CONFIG.account.hardware] || null;
},
currentPersona: () => {
if (!CONFIG.hardwarePersona.enabled || !CONFIG.account.hardware) return null;
return CONFIG.hardwarePersona.personas[CONFIG.account.hardware] || null;
},
// ---------- ENGINE ROTATION ----------
rollEngineForGame: () => {
const er = CONFIG.engineRotation;
if (!er.enabled) {
State.gameEngineOverride = null;
return CONFIG.engineType;
}
const w = er.weights;
const total = (w.api || 0) + (w.local_full || 0) + (w.local_shallow || 0);
if (total <= 0) {
State.gameEngineOverride = null;
return CONFIG.engineType;
}
const r = Math.random() * total;
let pick;
if (r < w.api) pick = 'api';
else if (r < w.api + w.local_full) pick = 'local_full';
else pick = 'local_shallow';
State.gameEngineOverride = pick;
Utils.log(`Engine rotation: this game uses ${pick}`, 'debug');
return pick;
},
// What engine should the analysis path use right now?
// Returns one of: 'api' | 'stockfish_online' | 'local'.
// Also returns a depth override (or null) if rotation forces shallow play.
currentEngine: () => {
const ovr = State.gameEngineOverride;
if (!ovr) return { type: CONFIG.engineType, depthCap: null };
if (ovr === 'api') return { type: 'api', depthCap: null };
if (ovr === 'local_full') return { type: 'local', depthCap: null };
if (ovr === 'local_shallow') return { type: 'local', depthCap: CONFIG.engineRotation.shallowDepth };
return { type: ovr, depthCap: null };
},
// ---------- HARD REPERTOIRE ----------
ensureRepertoire: () => {
const rh = CONFIG.repertoireHard;
if (!rh.enabled) return;
const acct = CONFIG.account;
if (acct.repertoire?.white && acct.repertoire?.black) return;
const pickN = (arr, n) => {
const copy = [...arr];
const out = [];
while (out.length < n && copy.length > 0) {
const idx = Math.floor(Math.random() * copy.length);
out.push(copy.splice(idx, 1)[0]);
}
return out;
};
acct.repertoire = acct.repertoire || { white: null, black: null };
if (!acct.repertoire.white) {
acct.repertoire.white = pickN(rh.whiteFirstMoves, rh.picksPerColor);
}
if (!acct.repertoire.black) {
acct.repertoire.black = {
e4: pickN(rh.blackVsE4, 1)[0],
d4: pickN(rh.blackVsD4, 1)[0],
other: pickN(rh.blackVsOther, 1)[0],
};
}
Utils.log(`Repertoire chosen: W=${JSON.stringify(acct.repertoire.white)} B=${JSON.stringify(acct.repertoire.black)}`, 'info');
Settings.save(CONFIG);
},
// Returns repertoire-preferred move for the current position, or null.
// Only invoked within the first 1-2 ply of a game.
repertoireMove: (fen, legalMoves) => {
const rh = CONFIG.repertoireHard;
if (!rh.enabled) return null;
if (Math.random() < rh.deviationRate) return null; // deviate
const acct = CONFIG.account;
if (!acct.repertoire) return null;
const fenParts = fen.split(' ');
const stm = fenParts[1]; // 'w' or 'b'
const fullmove = parseInt(fenParts[5] || '1');
// White move 1: pick from chosen white openings
if (stm === 'w' && fullmove === 1) {
const candidates = acct.repertoire.white || [];
// Translate first-move SAN to a UCI move that exists in legalMoves.
// We just match by piece destination — e.g. 'e4' = pawn to e4 = 'e2e4'
const sanToUci = { 'e4': 'e2e4', 'd4': 'd2d4', 'c4': 'c2c4', 'Nf3': 'g1f3' };
const ucis = candidates.map(s => sanToUci[s]).filter(Boolean);
const found = ucis.find(u => legalMoves.includes(u));
return found || null;
}
// Black move 1: pick response based on white's first move
if (stm === 'b' && fullmove === 1) {
const board = fenParts[0];
const ranks = board.split('/');
// ranks[0] = rank 8, ranks[6] = rank 2, ranks[4] = rank 4
const expandRank = (r) => {
let out = '';
for (const ch of r) out += /\d/.test(ch) ? ' '.repeat(parseInt(ch)) : ch;
return out;
};
const rank2 = expandRank(ranks[6]); // white's original pawn row
const rank4 = expandRank(ranks[4]); // two-up destination row
const rank3 = expandRank(ranks[5]); // one-up destination row (1.e3, 1.d3, 1.Nf3 etc)
let whiteFirst = 'other';
for (let f = 0; f < 8; f++) {
if (rank2[f] !== 'P') {
const file = 'abcdefgh'[f];
// Either pawn went 2 up (rank4) or 1 up (rank3); only care about e/d files
if ((rank4[f] === 'P' || rank3[f] === 'P') && (file === 'e' || file === 'd')) {
whiteFirst = file + '4'; // treat 1.e3 same as 1.e4 for repertoire purposes
}
break;
}
}
const blackChoice = acct.repertoire.black?.[whiteFirst] || acct.repertoire.black?.other;
if (!blackChoice) return null;
const sanToUci = {
'c5': 'c7c5', 'e5': 'e7e5', 'e6': 'e7e6', 'c6': 'c7c6',
'Nf6': 'g8f6', 'd5': 'd7d5',
};
const uci = sanToUci[blackChoice];
return (uci && legalMoves.includes(uci)) ? uci : null;
}
return null;
},
// ---------- TC LOCK ----------
// Reads TC label like "3 min", "10 | 5", "Bullet" from the page header.
readTimeControl: () => {
try {
const sels = [
'.cc-game-header-time-control',
'[data-test-element="game-info-time-control"]',
'.game-info-section-time-control',
'.live-game-time-control',
];
for (const s of sels) {
const el = document.querySelector(s);
if (el && el.textContent.trim()) return el.textContent.trim();
}
// Fallback: grab anything that smells like "X min" or "X | Y"
const candidates = document.querySelectorAll('span, div');
for (const c of candidates) {
if (c.children.length) continue;
const t = (c.textContent || '').trim();
if (/^\d{1,3}\s*(min|m)$/i.test(t)) return t;
if (/^\d{1,3}\s*\|\s*\d{1,3}$/.test(t)) return t;
}
} catch (e) {}
return null;
},
// Called from auto-queue. Returns true if it's OK to queue another game.
tcLockAllowsQueue: () => {
if (!CONFIG.tcLock.enabled) return true;
const current = Account.readTimeControl();
if (!current) return true; // can't read → don't block
if (!CONFIG.account.sessionTC) {
CONFIG.account.sessionTC = current;
Settings.save(CONFIG);
Utils.log(`TC lock: session locked to "${current}"`, 'info');
return true;
}
if (current !== CONFIG.account.sessionTC) {
Utils.log(`TC lock: skipping queue (current "${current}" != session "${CONFIG.account.sessionTC}")`, 'warn');
return false;
}
return true;
},
// Called when starting a new session (after break, fresh page load)
clearSessionTC: () => {
CONFIG.account.sessionTC = null;
Settings.save(CONFIG);
},
};
// ═══════════════════════════════════════════
// RATING PROFILE SYSTEM
// ═══════════════════════════════════════════
const RatingProfile = {
// Play style modifiers (multiply suboptimal rates per phase)
styles: {
universal: { opening: 1.0, middlegame: 1.0, endgame: 1.0, preferTactical: 0.5 },
aggressive: { opening: 0.7, middlegame: 0.8, endgame: 1.3, preferTactical: 0.8 },
positional: { opening: 1.2, middlegame: 1.1, endgame: 0.8, preferTactical: 0.2 },
endgame_specialist: { opening: 1.5, middlegame: 1.3, endgame: 0.4, preferTactical: 0.5 },
},
// Map target rating to all humanization parameters.
//
// Correlation targets are calibrated against Lichess / chess.com public data:
// 800 ELO real players match SF top-1 ~35-45% of the time
// 1500 ELO real players match SF top-1 ~45-55%
// 2000 ELO real players match SF top-1 ~55-62%
// 2400 ELO real players match SF top-1 ~62-68%
// 2800 ELO (Magnus) matches SF top-1 ~65-72% peak, rarely above
//
// The previous profile targeted 92% correlation at 2800 which is IMPOSSIBLE
// for a human and guaranteed chess.com fair-play review within ~10 games.
apply(targetRating) {
const r = Math.max(800, Math.min(2800, targetRating));
const t = (r - 800) / 2000; // 0.0 (800) to 1.0 (2800)
const lerp = (a, b, t) => a + (b - a) * t;
const style = RatingProfile.styles[CONFIG.playStyle] || RatingProfile.styles.universal;
// Engine depth: keep this moderate even at high rating. We don't need
// depth 20 to pick a 2200-quality move — depth 14-16 is plenty and
// avoids the telltale "all top-n moves are SF depth 20 favorites" pattern.
CONFIG.engineDepth.base = Math.round(lerp(8, 16, t));
CONFIG.engineDepth.min = Math.max(6, CONFIG.engineDepth.base - 3);
CONFIG.engineDepth.max = Math.min(20, CONFIG.engineDepth.base + 3);
// Target engine correlation: 42% at 800 → 68% at 2800 (was 40% → 92%)
CONFIG.humanization.targetEngineCorrelation = lerp(0.42, 0.68, t);
// Suboptimal rates — floor raised so we never play sub-8% suboptimal,
// even at 2800. Real 2400+ players still play suboptimal 15-20% of the time
// against Stockfish 18 benchmark.
CONFIG.humanization.suboptimalMoveRate.opening = lerp(0.42, 0.12, t) * style.opening;
CONFIG.humanization.suboptimalMoveRate.middlegame = lerp(0.48, 0.18, t) * style.middlegame;
CONFIG.humanization.suboptimalMoveRate.endgame = lerp(0.38, 0.10, t) * style.endgame;
// Max CP loss — tight throughout. Hanging pieces IS the ban signal.
CONFIG.humanization.maxAcceptableCPLoss.opening = Math.round(lerp(80, 25, t));
CONFIG.humanization.maxAcceptableCPLoss.middlegame = Math.round(lerp(110, 35, t));
CONFIG.humanization.maxAcceptableCPLoss.endgame = Math.round(lerp(75, 20, t));
// Blunder chance: 3% at 800 → 0.3% at 2800 (was 4% → 0.1%).
// Floor raised to 0.3% so even GM-profile has occasional slips.
CONFIG.humanization.blunder.chance = lerp(0.03, 0.003, t);
CONFIG.humanization.blunder.maxCPLoss = Math.round(lerp(180, 80, t));
// Streaks: cap perfect run lower so we don't chain 12 best moves.
CONFIG.humanization.streaks.perfectStreakMax = Math.round(lerp(3, 7, t));
CONFIG.humanization.streaks.sloppyStreakMax = Math.round(lerp(3, 1, t));
// maxTopMoveRate: HARD cap. Was 0.50→0.95, now 0.45→0.68.
// This is the single most important anti-ban parameter — it bounds
// how often we can EVER play SF#1 regardless of any other logic.
if (CONFIG.humanization.antiCorrelation) {
CONFIG.humanization.antiCorrelation.maxTopMoveRate = lerp(0.45, 0.68, t);
}
// Timing: slower at low rating, but keep a human floor (800 elo players
// still sometimes blitz out moves in ~1s).
CONFIG.timing.base.min = Math.round(lerp(2000, 900, t));
CONFIG.timing.base.max = Math.round(lerp(6000, 3200, t));
CONFIG.timing.complex.min = Math.round(lerp(4200, 2200, t));
CONFIG.timing.complex.max = Math.round(lerp(11000, 5800, t));
CONFIG.timing.longThink.chance = lerp(0.14, 0.07, t);
Utils.log(`Rating profile applied: ${r} ELO (style: ${CONFIG.playStyle}) | target corr ${(CONFIG.humanization.targetEngineCorrelation*100).toFixed(0)}% | SF#1 cap ${(CONFIG.humanization.antiCorrelation.maxTopMoveRate*100).toFixed(0)}%`, 'info');
Settings.save(CONFIG);
}
};
// ═══════════════════════════════════════════
// SELECTORS
// ═══════════════════════════════════════════
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, .clock-time-monospace',
gameOver: [
'[data-cy="game-over-modal-content"]',
'.game-over-modal-shell-content',
'.game-over-modal-container',
'.modal-game-over-component',
'[data-cy="game-over-modal"]',
'.game-over-modal-buttons',
'.game-over-buttons-component',
'.game-result-header'
],
gameOverResult: {
win: [
'.game-over-modal-header-userWon',
'[data-cy="header-title-component"]:not(:empty)',
],
loss: [
'.game-over-modal-header-userLost',
],
draw: [
'.game-over-modal-header-draw',
],
},
drawOffer: '.draw-offer-component',
promotion: {
dialog: '.promotion-window, .promotion-piece',
items: '.promotion-piece'
}
};
// ═══════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════
const State = {
engineReady: false,
apiAvailable: true, // assume API works until proven otherwise
apiFailCount: 0,
// Per-game engine override (set by Account.rollEngineForGame).
// 'api' | 'stockfish_online' | 'local_full' | 'local_shallow' | null
gameEngineOverride: null,
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,
openingMoves: [], // track opening moves for repertoire consistency
},
candidates: {},
currentEval: null,
currentBestMove: null,
opponentResponse: null,
apiResult: null, // result from API engine
// Clock tracking
clock: {
myTime: null, // seconds remaining
oppTime: null,
},
// Recent move timings for sequence variation
recentTimings: [],
// Humanization tracking
human: {
perfectStreak: 0,
sloppyStreak: 0,
bestMoveCount: 0,
totalMoveCount: 0,
gamePersonality: null,
lastMoveWasBest: true,
// Accuracy cluster state
clusterMode: 'normal', // 'normal', 'hot', 'cold'
clusterMovesLeft: 0,
// Predicted opponent reply (for premove simulation)
predictedReply: null,
predictedReplyFen: null,
// Auto-resign: track consecutive losing evals
consecutiveLosingEvals: 0,
// Auto-lose mode active flag
autoLoseActive: false,
// Opponent rating (read from DOM)
opponentRating: null,
// Time pressure accuracy multipliers (live)
timePressureMult: { suboptimal: 1, blunder: 1, maxCPLoss: 1 },
// Anti-correlation: track how often we play SF#1
topMoveCount: 0,
// Weakness profile (generated from seed)
weaknesses: null,
// Timing-accuracy coupling: pre-decided think time category for current move
thinkCategory: 'normal', // 'fast', 'normal', 'slow'
// Opponent-move surprise: was the opponent's last move expected?
opponentMoveSurprise: 0, // 0 = expected, 1 = surprising, set each move
// Think momentum: how long we thought last move (ms)
lastThinkTime: 0,
// Track the eval BEFORE opponent moved, to measure surprise
evalBeforeOpponentMove: null,
// Persistent player tempo (seeded per account, set once)
playerTempo: 1.0, // multiplier: <1 = fast player, >1 = slow player
playerAccuracyBand: 0, // small offset to base suboptimal rate
},
// Player move DB cache (FEN -> moves array)
playerDBCache: new Map(),
// Track if last move was a capture (for recapture detection)
lastMoveWasCapture: false,
};
// ═══════════════════════════════════════════
// UTILITIES
// ═══════════════════════════════════════════
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,
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;
},
humanDelay: (min, max) => {
const median = (min + max) / 2;
const sigma = 0.6;
const logNormal = Math.exp(Utils.gaussianRandom(Math.log(median), sigma));
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);
},
countPieces: (fen) => {
if (!fen) return 32;
const board = fen.split(' ')[0];
return (board.match(/[rnbqkpRNBQKP]/g) || []).length;
},
};
// ═══════════════════════════════════════════
// WEAKNESS PROFILE — persistent per-account human identity
// ═══════════════════════════════════════════
const WeaknessProfile = {
// Seeded PRNG (mulberry32) for deterministic weakness generation
_prng: (seed) => {
let s = seed | 0;
return () => {
s = (s + 0x6D2B79F5) | 0;
let t = Math.imul(s ^ (s >>> 15), 1 | s);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
},
// All possible weakness dimensions a human can have
_dimensions: {
// Piece-type weaknesses: worse at using/defending specific pieces
pieces: ['knight', 'bishop', 'rook', 'queen'],
// Phase weaknesses: worse in specific game phases
phases: ['opening', 'middlegame', 'endgame'],
// Endgame-type weaknesses
endgames: ['rook_endgame', 'bishop_endgame', 'knight_endgame', 'pawn_endgame', 'queen_endgame'],
// Tactical motif blind spots
tactics: ['fork', 'pin', 'skewer', 'discovery', 'back_rank', 'deflection'],
// Positional blind spots
positional: ['pawn_structure', 'king_safety', 'piece_activity', 'space', 'weak_squares'],
},
init: () => {
const wp = CONFIG.humanization.weaknessProfile;
if (!wp.enabled) {
State.human.weaknesses = { pieces: [], phases: {}, endgames: [], tactics: [], positional: [], extraErrorRate: {} };
return;
}
// Generate or load seed
if (!wp.seed) {
wp.seed = Math.floor(Math.random() * 2147483647);
Settings.save(CONFIG);
Utils.log(`WeaknessProfile: Generated new seed ${wp.seed}`, 'info');
}
const rng = WeaknessProfile._prng(wp.seed);
const pick = (arr, count) => {
const shuffled = [...arr].sort(() => rng() - 0.5);
return shuffled.slice(0, count);
};
// Each account gets 1-2 piece weaknesses, 1 phase weakness, 1-2 endgame weaknesses,
// 1-2 tactical blind spots, 1 positional weakness
const d = WeaknessProfile._dimensions;
const weakPieces = pick(d.pieces, 1 + (rng() < 0.4 ? 1 : 0));
const weakEndgames = pick(d.endgames, 1 + (rng() < 0.5 ? 1 : 0));
const weakTactics = pick(d.tactics, 1 + (rng() < 0.35 ? 1 : 0));
const weakPositional = pick(d.positional, 1);
// Phase weakness: one phase is noticeably worse
const phaseWeights = {};
for (const p of d.phases) {
phaseWeights[p] = 1.0; // baseline
}
const worstPhase = pick(d.phases, 1)[0];
phaseWeights[worstPhase] = 1.15 + rng() * 0.25; // 1.15-1.40x more errors in weak phase
// Generate extra error rates for each weakness (how much worse they are)
const extraError = {};
for (const p of weakPieces) extraError[`piece_${p}`] = 0.04 + rng() * 0.05;
for (const e of weakEndgames) extraError[`endgame_${e}`] = 0.05 + rng() * 0.06;
for (const t of weakTactics) extraError[`tactic_${t}`] = 0.05 + rng() * 0.05;
for (const p of weakPositional) extraError[`positional_${p}`] = 0.03 + rng() * 0.04;
State.human.weaknesses = {
pieces: weakPieces,
phases: phaseWeights,
endgames: weakEndgames,
tactics: weakTactics,
positional: weakPositional,
extraErrorRate: extraError,
};
// --- Multi-game behavioral consistency (item 5) ---
// Generate persistent player tempo and accuracy band from the same seed
// These make the account feel like a consistent person across many games
State.human.playerTempo = 0.80 + rng() * 0.40; // 0.80-1.20 (fast player vs slow player)
State.human.playerAccuracyBand = -0.04 + rng() * 0.08; // -0.04 to +0.04 shift on suboptimal rate
Utils.log(`WeaknessProfile [seed=${wp.seed}]: pieces=${weakPieces}, phase=${worstPhase}(x${phaseWeights[worstPhase].toFixed(2)}), endgames=${weakEndgames}, tactics=${weakTactics}, positional=${weakPositional}, tempo=${State.human.playerTempo.toFixed(2)}, accuracyBand=${State.human.playerAccuracyBand.toFixed(3)}`, 'info');
},
// Returns extra suboptimal rate for the current position based on weaknesses
getExtraErrorRate: (fen, move) => {
const w = State.human.weaknesses;
if (!w || !CONFIG.humanization.weaknessProfile.enabled) return 0;
let extra = 0;
const phase = HumanStrategy.getGamePhase(fen);
// Phase weakness multiplier (applied as a multiplier to total rate externally)
// Here we return additive bonus
const phaseBonus = (w.phases[phase] || 1.0) - 1.0;
extra += phaseBonus * 0.06; // convert multiplier to small additive rate
// Piece involvement: check if the moving piece matches a weakness
if (move && move.length >= 4) {
const fromSq = move.substring(0, 2);
const movingPiece = WeaknessProfile._identifyPiece(fen, fromSq);
if (movingPiece && w.pieces.includes(movingPiece)) {
extra += w.extraErrorRate[`piece_${movingPiece}`] || 0;
}
}
// Endgame type weakness
if (phase === 'endgame') {
const pieces = Utils.countPieces(fen);
const egType = WeaknessProfile._classifyEndgame(fen);
if (egType && w.endgames.includes(egType)) {
extra += w.extraErrorRate[`endgame_${egType}`] || 0;
}
}
return Math.min(extra, 0.15); // cap total extra at 15%
},
_identifyPiece: (fen, sq) => {
const board = fen.split(' ')[0];
const file = sq.charCodeAt(0) - 97;
const rank = parseInt(sq[1]) - 1;
const rows = board.split('/').reverse();
if (!rows[rank]) return null;
let col = 0;
for (const ch of rows[rank]) {
if (/\d/.test(ch)) { col += parseInt(ch); continue; }
if (col === file) {
const map = { n: 'knight', b: 'bishop', r: 'rook', q: 'queen', k: 'king', p: 'pawn' };
return map[ch.toLowerCase()] || null;
}
col++;
}
return null;
},
_classifyEndgame: (fen) => {
const board = fen.split(' ')[0].toLowerCase();
const hasQ = board.includes('q');
const hasR = board.includes('r');
const hasB = board.includes('b');
const hasN = board.includes('n');
if (hasQ && !hasR && !hasB && !hasN) return 'queen_endgame';
if (hasR && !hasQ && !hasB && !hasN) return 'rook_endgame';
if (hasB && !hasQ && !hasR && !hasN) return 'bishop_endgame';
if (hasN && !hasQ && !hasR && !hasB) return 'knight_endgame';
if (!hasQ && !hasR && !hasB && !hasN) return 'pawn_endgame';
return null; // mixed — no specific type
},
};
// ═══════════════════════════════════════════
// PLAYER MOVE DATABASE — Lichess rating-filtered human moves
// ═══════════════════════════════════════════
const PlayerMoveDB = {
fetch: (fen) => {
const cfg = CONFIG.humanization.playerMoveDB;
if (!cfg.enabled) return Promise.resolve(null);
const cacheKey = fen.split(' ').slice(0, 4).join(' ');
if (State.playerDBCache.has(cacheKey)) {
return Promise.resolve(State.playerDBCache.get(cacheKey));
}
const rating = CONFIG.targetRating;
const minR = Math.max(400, rating - cfg.ratingWindow);
const maxR = Math.min(2800, rating + cfg.ratingWindow);
// Lichess explorer API: player rating-filtered games
const ratingStr = `${minR},${maxR}`;
const url = `https://explorer.lichess.ovh/lichess?fen=${encodeURIComponent(fen)}&ratings=${encodeURIComponent(ratingStr)}&speeds=blitz,rapid,classical&topGames=0&recentGames=0`;
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
timeout: cfg.timeout,
onload: (response) => {
try {
const data = JSON.parse(response.responseText);
if (data.moves && data.moves.length > 0) {
const validMoves = data.moves
.map(m => ({
uci: m.uci,
games: m.white + m.draws + m.black,
winRate: m.white / Math.max(1, m.white + m.draws + m.black),
}))
.filter(m => m.games >= cfg.minGames);
if (validMoves.length > 0) {
State.playerDBCache.set(cacheKey, validMoves);
Utils.log(`PlayerMoveDB: ${validMoves.length} moves at ${minR}-${maxR} ELO`, 'debug');
resolve(validMoves);
return;
}
}
State.playerDBCache.set(cacheKey, null);
resolve(null);
} catch (e) {
resolve(null);
}
},
onerror: () => resolve(null),
ontimeout: () => resolve(null),
});
});
},
// Pick a move from the player DB weighted by game count
pickMove: (moves) => {
if (!moves || moves.length === 0) return null;
const totalGames = moves.reduce((s, m) => s + m.games, 0);
let r = Math.random() * totalGames;
for (const m of moves) {
r -= m.games;
if (r <= 0) return m.uci;
}
return moves[0].uci;
},
};
// ═══════════════════════════════════════════
// UI
// ═══════════════════════════════════════════
const UI = {
_styleAccents: {
universal: '#4caf50',
aggressive: '#ff5722',
positional: '#2196f3',
endgame_specialist: '#9c27b0',
},
setAccent: () => {
const color = UI._styleAccents[CONFIG.playStyle] || '#4caf50';
document.documentElement.style.setProperty('--ba-accent', color);
},
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; }
:root { --ba-accent: #4caf50; }
.ba-panel {
position: fixed; top: 50px; left: 50px; z-index: 10001;
width: 320px;
background: rgba(10, 10, 12, 0.95);
color: #e0e0e0;
border: 1px solid #333;
border-left: 2px solid var(--ba-accent);
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, color-mix(in srgb, var(--ba-accent) 10%, transparent), 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: var(--ba-accent); }
.ba-logo span { color: #fff; opacity: 0.7; font-weight: 400; }
.ba-minimize { cursor: pointer; opacity: 0.5; transition: 0.2s; font-size: 16px; }
.ba-minimize:hover { opacity: 1; color: #fff; }
.ba-tabs { display: flex; background: rgba(0,0,0,0.2); flex-wrap: wrap; }
.ba-tab {
flex: 1; text-align: center; padding: 8px 0;
font-size: 10px; font-weight: 600; color: #666;
cursor: pointer; transition: 0.2s;
border-bottom: 2px solid transparent;
min-width: 60px;
}
.ba-tab:hover { color: #aaa; background: rgba(255,255,255,0.02); }
.ba-tab.active { color: #e0e0e0; border-bottom: 2px solid var(--ba-accent); background: color-mix(in srgb, var(--ba-accent) 5%, transparent); }
.ba-content { padding: 14px; min-height: 150px; max-height: 420px; 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: 11px; color: #aaa; }
.ba-value { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--ba-accent); }
.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: var(--ba-accent); border-radius: 50%; cursor: pointer;
box-shadow: 0 0 10px color-mix(in srgb, var(--ba-accent) 40%, transparent);
}
.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;
flex-shrink: 0;
}
.ba-checkbox.checked { background: var(--ba-accent); border-color: var(--ba-accent); }
.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: 14px; 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: var(--ba-accent); background: color-mix(in srgb, var(--ba-accent) 10%, transparent); padding: 4px 8px; border-radius: 4px; display: inline-block; }
.ba-engine-badge {
font-size: 9px; padding: 2px 6px; border-radius: 3px; margin-top: 6px; display: inline-block;
background: color-mix(in srgb, var(--ba-accent) 15%, transparent); color: var(--ba-accent); font-family: 'JetBrains Mono';
}
.ba-stats-row { display: flex; justify-content: space-between; margin-bottom: 6px; padding: 4px 8px; background: rgba(255,255,255,0.02); border-radius: 4px; }
.ba-stats-label { font-size: 10px; color: #888; }
.ba-stats-value { font-size: 10px; color: #ccc; font-family: 'JetBrains Mono'; }
.ba-select {
background: #1a1a1a; color: #e0e0e0; border: 1px solid #444;
border-radius: 4px; padding: 4px 8px; font-size: 11px;
font-family: 'Inter', sans-serif; cursor: pointer; outline: none;
}
.ba-select:focus { border-color: var(--ba-accent); }
input[type="color"] { -webkit-appearance: none; border: none; }
input[type="color"]::-webkit-color-swatch-wrapper { padding: 2px; }
input[type="color"]::-webkit-color-swatch { border: none; border-radius: 3px; }
.ba-section-title { font-size: 10px; font-weight: 600; color: #666; text-transform: uppercase; letter-spacing: 1px; margin: 12px 0 8px 0; }
.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; }
.ba-minimized .ba-tabs, .ba-minimized .ba-content, .ba-minimized .ba-footer { display: none; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
.ba-arrow { stroke-linecap: round; filter: drop-shadow(0 0 3px rgba(0,0,0,0.4)); }
@keyframes ba-dash-flow { from { stroke-dashoffset: 16; } to { stroke-dashoffset: 0; } }
@keyframes ba-pulse-ring { 0%,100% { opacity: 0.5; } 50% { opacity: 0.9; } }
@keyframes ba-fade-in { from { opacity: 0; } to { opacity: 1; } }
.ba-arrow-group { animation: ba-fade-in 0.25s ease-out; }
.ba-dash-anim { animation: ba-dash-flow 0.6s linear infinite; }
.ba-pulse { animation: ba-pulse-ring 1.8s ease-in-out infinite; }
.ba-toast-container {
position: fixed; bottom: 16px; right: 16px; z-index: 100001;
display: flex; flex-direction: column-reverse; gap: 8px; pointer-events: none;
max-height: 50vh; overflow: hidden;
}
.ba-toast {
pointer-events: auto;
font-family: 'Inter', 'Segoe UI', sans-serif;
font-size: 12px; line-height: 1.4;
padding: 10px 14px 10px 12px;
border-radius: 8px;
color: #e0e0e0;
background: #1a1a2eee;
border-left: 3px solid var(--ba-accent);
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
backdrop-filter: blur(8px);
display: flex; align-items: flex-start; gap: 8px;
max-width: 320px; min-width: 200px;
animation: ba-toast-in 0.3s ease-out;
position: relative; overflow: hidden;
cursor: pointer;
}
.ba-toast:hover { opacity: 0.85; }
.ba-toast-icon { font-size: 14px; flex-shrink: 0; margin-top: 1px; }
.ba-toast-body { flex: 1; }
.ba-toast-title { font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
.ba-toast-msg { color: #bbb; font-size: 11.5px; }
.ba-toast-progress {
position: absolute; bottom: 0; left: 0; height: 2px;
background: currentColor; opacity: 0.4;
animation: ba-toast-timer linear forwards;
}
.ba-toast.ba-toast-info { border-left-color: #42a5f5; }
.ba-toast.ba-toast-warn { border-left-color: #ffb74d; }
.ba-toast.ba-toast-error { border-left-color: #ef5350; }
.ba-toast.ba-toast-success { border-left-color: #66bb6a; }
.ba-toast.ba-toast-move { border-left-color: #ab47bc; }
.ba-toast-info .ba-toast-title { color: #42a5f5; }
.ba-toast-warn .ba-toast-title { color: #ffb74d; }
.ba-toast-error .ba-toast-title { color: #ef5350; }
.ba-toast-success .ba-toast-title { color: #66bb6a; }
.ba-toast-move .ba-toast-title { color: #ab47bc; }
@keyframes ba-toast-in { from { opacity: 0; transform: translateX(60px); } to { opacity: 1; transform: translateX(0); } }
@keyframes ba-toast-out { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(60px); } }
@keyframes ba-toast-timer { from { width: 100%; } to { width: 0%; } }
`);
},
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 v15.1</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="engine">ENGINE</div>
<div class="ba-tab" data-tab="timings">TIMINGS</div>
<div class="ba-tab" data-tab="safety">SAFETY</div>
<div class="ba-tab" data-tab="visuals">VISUALS</div>
<div class="ba-tab" data-tab="stats">STATS</div>
</div>
<div class="ba-content">
<!-- MAIN TAB -->
<div id="tab-main" class="ba-page active">
<div class="ba-status-box">
<span class="ba-eval-large" id="eval-display">0.00</span>
<span class="ba-best-move-large" id="move-display">Waiting...</span>
<div class="ba-engine-badge" id="engine-badge">LOCAL SF10</div>
</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 class="ba-row">
<span class="ba-label">Tablebase (Endgame)</span>
<div class="ba-checkbox ${CONFIG.useTablebase ? 'checked' : ''}" id="toggle-tablebase"></div>
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Target Rating</span>
<span class="ba-value" id="val-rating">${CONFIG.targetRating}</span>
</div>
<input type="range" class="ba-slider" min="800" max="2800" step="50" value="${CONFIG.targetRating}" id="slide-rating">
</div>
<div class="ba-row">
<span class="ba-label">Play Style</span>
<select class="ba-select" id="select-style">
<option value="universal" ${CONFIG.playStyle === 'universal' ? 'selected' : ''}>Universal</option>
<option value="aggressive" ${CONFIG.playStyle === 'aggressive' ? 'selected' : ''}>Aggressive</option>
<option value="positional" ${CONFIG.playStyle === 'positional' ? 'selected' : ''}>Positional</option>
<option value="endgame_specialist" ${CONFIG.playStyle === 'endgame_specialist' ? 'selected' : ''}>Endgame Specialist</option>
</select>
</div>
</div>
<!-- ENGINE TAB -->
<div id="tab-engine" class="ba-page">
<div class="ba-section-title">Engine Source</div>
<div class="ba-row">
<span class="ba-label">Primary Engine</span>
<select class="ba-select" id="select-engine">
<option value="api" ${CONFIG.engineType === 'api' ? 'selected' : ''}>chess-api.com (SF18)</option>
<option value="stockfish_online" ${CONFIG.engineType === 'stockfish_online' ? 'selected' : ''}>stockfish.online (SF16)</option>
<option value="local" ${CONFIG.engineType === 'local' ? 'selected' : ''}>Local SF10 (Fallback)</option>
</select>
</div>
<div class="ba-section-title">Depth</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Base Depth</span>
<span class="ba-value" id="val-depth">${CONFIG.engineDepth.base}</span>
</div>
<input type="range" class="ba-slider" min="4" max="22" value="${CONFIG.engineDepth.base}" id="slide-depth">
</div>
<div class="ba-row">
<span class="ba-label">Dynamic Depth</span>
<div class="ba-checkbox ${CONFIG.engineDepth.dynamicDepth ? 'checked' : ''}" id="toggle-dynamic-depth"></div>
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Multi-PV Lines</span>
<span class="ba-value" id="val-mpv">${CONFIG.multiPV}</span>
</div>
<input type="range" class="ba-slider" min="1" max="8" value="${CONFIG.multiPV}" id="slide-mpv">
</div>
</div>
<!-- TIMINGS TAB -->
<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="30" 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="5000" 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="12000" 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="70" value="${Math.round(CONFIG.humanization.suboptimalMoveRate.middlegame * 100)}" id="slide-subopt">
</div>
<div class="ba-row">
<span class="ba-label">Clock-Aware Timing</span>
<div class="ba-checkbox ${CONFIG.timing.clockAware.enabled ? 'checked' : ''}" id="toggle-clock-aware"></div>
</div>
<div class="ba-row">
<span class="ba-label">Premove Simulation</span>
<div class="ba-checkbox ${CONFIG.timing.premove.enabled ? 'checked' : ''}" id="toggle-premove"></div>
</div>
</div>
<!-- SAFETY TAB -->
<div id="tab-safety" class="ba-page">
<div class="ba-section-title">Anti-Detection</div>
<div class="ba-row">
<span class="ba-label">Random AFK Pauses</span>
<div class="ba-checkbox ${CONFIG.antiDetection.randomAFK.enabled ? 'checked' : ''}" id="toggle-random-afk"></div>
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">AFK Chance %</span>
<span class="ba-value" id="val-afk-chance">${Math.round(CONFIG.antiDetection.randomAFK.chance * 100)}</span>
</div>
<input type="range" class="ba-slider" min="1" max="20" value="${Math.round(CONFIG.antiDetection.randomAFK.chance * 100)}" id="slide-afk-chance">
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Random Legal Move %</span>
<span class="ba-value" id="val-random-legal">${Math.round(CONFIG.antiDetection.randomLegalMoveChance * 100)}</span>
</div>
<input type="range" class="ba-slider" min="0" max="15" value="${Math.round(CONFIG.antiDetection.randomLegalMoveChance * 100)}" id="slide-random-legal">
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Max Games / Hour</span>
<span class="ba-value" id="val-max-gph">${CONFIG.antiDetection.maxGamesPerHour}</span>
</div>
<input type="range" class="ba-slider" min="2" max="15" value="${CONFIG.antiDetection.maxGamesPerHour}" id="slide-max-gph">
</div>
<div class="ba-section-title">Session</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Max Games / Session</span>
<span class="ba-value" id="val-max-session">${CONFIG.session.maxGamesPerSession}</span>
</div>
<input type="range" class="ba-slider" min="2" max="20" value="${CONFIG.session.maxGamesPerSession}" id="slide-max-session">
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Break Duration (min)</span>
<span class="ba-value" id="val-break-dur">${Math.round(CONFIG.session.breakDurationMs / 60000)}</span>
</div>
<input type="range" class="ba-slider" min="1" max="15" value="${Math.round(CONFIG.session.breakDurationMs / 60000)}" id="slide-break-dur">
</div>
<div class="ba-row">
<span class="ba-label">Auto-Queue</span>
<div class="ba-checkbox ${CONFIG.auto.autoQueue ? 'checked' : ''}" id="toggle-auto-queue"></div>
</div>
<div class="ba-section-title">Auto-Lose</div>
<div class="ba-row">
<span class="ba-label">Enabled</span>
<div class="ba-checkbox ${CONFIG.autoLose.enabled ? 'checked' : ''}" id="toggle-auto-lose"></div>
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Trigger Win Streak</span>
<span class="ba-value" id="val-lose-streak">${CONFIG.autoLose.triggerStreak}</span>
</div>
<input type="range" class="ba-slider" min="2" max="10" value="${CONFIG.autoLose.triggerStreak}" id="slide-lose-streak">
</div>
<div class="ba-section-title">Auto-Resign</div>
<div class="ba-row">
<span class="ba-label">Enabled</span>
<div class="ba-checkbox ${CONFIG.autoResign.enabled ? 'checked' : ''}" id="toggle-auto-resign"></div>
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Resign Eval Threshold</span>
<span class="ba-value" id="val-resign-eval">${CONFIG.autoResign.evalThreshold}</span>
</div>
<input type="range" class="ba-slider" min="-10" max="-2" step="0.5" value="${CONFIG.autoResign.evalThreshold}" id="slide-resign-eval">
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Resign Chance %</span>
<span class="ba-value" id="val-resign-chance">${Math.round(CONFIG.autoResign.resignChance * 100)}</span>
</div>
<input type="range" class="ba-slider" min="10" max="100" value="${Math.round(CONFIG.autoResign.resignChance * 100)}" id="slide-resign-chance">
</div>
<div class="ba-section-title">Opponent Adaptation</div>
<div class="ba-row">
<span class="ba-label">Enabled</span>
<div class="ba-checkbox ${CONFIG.opponentAdaptation.enabled ? 'checked' : ''}" id="toggle-opp-adapt"></div>
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Rating Edge</span>
<span class="ba-value" id="val-rating-edge">${CONFIG.opponentAdaptation.ratingEdge}</span>
</div>
<input type="range" class="ba-slider" min="50" max="500" step="25" value="${CONFIG.opponentAdaptation.ratingEdge}" id="slide-rating-edge">
</div>
<div class="ba-section-title">Time Pressure</div>
<div class="ba-row">
<span class="ba-label">Accuracy Drop</span>
<div class="ba-checkbox ${CONFIG.timePressure.enabled ? 'checked' : ''}" id="toggle-time-pressure"></div>
</div>
<div class="ba-section-title">Humanization</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Blunder Chance %</span>
<span class="ba-value" id="val-blunder">${(CONFIG.humanization.blunder.chance * 100).toFixed(1)}</span>
</div>
<input type="range" class="ba-slider" min="0" max="15" step="0.5" value="${(CONFIG.humanization.blunder.chance * 100).toFixed(1)}" id="slide-blunder">
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Max Win Streak</span>
<span class="ba-value" id="val-max-streak">${CONFIG.session.maxWinStreak}</span>
</div>
<input type="range" class="ba-slider" min="2" max="10" value="${CONFIG.session.maxWinStreak}" id="slide-max-streak">
</div>
<div class="ba-section-title">Anti-Detection Layers</div>
<div class="ba-row" title="Master switch for all humanization. OFF = pure engine play (= instant ban).">
<span class="ba-label">Humanization Master</span>
<div class="ba-checkbox ${CONFIG.humanization.enabled ? 'checked' : ''}" id="toggle-human-master"></div>
</div>
<div class="ba-row" title="Vary streaks of best moves and sloppy moves so we don't chain perfect play forever.">
<span class="ba-label">Streak Limiter</span>
<div class="ba-checkbox ${CONFIG.humanization.streaks.enabled ? 'checked' : ''}" id="toggle-streaks"></div>
</div>
<div class="ba-row" title="Cluster accuracy across moves so games don't all look identical.">
<span class="ba-label">Accuracy Clustering</span>
<div class="ba-checkbox ${CONFIG.humanization.accuracyClustering.enabled ? 'checked' : ''}" id="toggle-clustering"></div>
</div>
<div class="ba-row" title="Persistent per-account 'weakness' (e.g. weaker in endgames). Makes profile coherent over time.">
<span class="ba-label">Weakness Profile</span>
<div class="ba-checkbox ${CONFIG.humanization.weaknessProfile.enabled ? 'checked' : ''}" id="toggle-weakness"></div>
</div>
<div class="ba-row" title="Use Lichess explorer to pick moves real human players make in the same position.">
<span class="ba-label">Player Move DB</span>
<div class="ba-checkbox ${CONFIG.humanization.playerMoveDB.enabled ? 'checked' : ''}" id="toggle-playerdb"></div>
</div>
<div class="ba-row" title="Couple thinking time and move quality (long think -> better move).">
<span class="ba-label">Timing Coupling</span>
<div class="ba-checkbox ${CONFIG.humanization.timingAccuracyCoupling.enabled ? 'checked' : ''}" id="toggle-timing-couple"></div>
</div>
<div class="ba-row" title="Occasionally start dragging toward the wrong square then redirect.">
<span class="ba-label">Change-of-Mind Drag</span>
<div class="ba-checkbox ${CONFIG.antiDetection.changeOfMind.enabled ? 'checked' : ''}" id="toggle-cofmind"></div>
</div>
<div class="ba-row" title="Background mouse hovers, premove flicks, UI clicks to look like an active player.">
<span class="ba-label">Telemetry Noise</span>
<div class="ba-checkbox ${CONFIG.antiDetection.telemetryNoise.enabled ? 'checked' : ''}" id="toggle-telemetry"></div>
</div>
<div class="ba-slider-container" title="Hard cap on how often we may play SF#1. Lower = safer but weaker.">
<div class="ba-slider-header">
<span class="ba-label">SF#1 Move Cap %</span>
<span class="ba-value" id="val-topcap">${Math.round(CONFIG.humanization.antiCorrelation.maxTopMoveRate * 100)}</span>
</div>
<input type="range" class="ba-slider" min="35" max="80" value="${Math.round(CONFIG.humanization.antiCorrelation.maxTopMoveRate * 100)}" id="slide-topcap">
</div>
<div class="ba-slider-container" title="+/- variation on session length so we don't always quit at the same game number.">
<div class="ba-slider-header">
<span class="ba-label">Session Jitter %</span>
<span class="ba-value" id="val-sjitter">${Math.round(CONFIG.antiDetection.sessionLengthJitter * 100)}</span>
</div>
<input type="range" class="ba-slider" min="0" max="60" value="${Math.round(CONFIG.antiDetection.sessionLengthJitter * 100)}" id="slide-sjitter">
</div>
<div class="ba-slider-container" title="Minimum delay between consecutive games (lower bound, in seconds).">
<div class="ba-slider-header">
<span class="ba-label">Between-Game Min (s)</span>
<span class="ba-value" id="val-bgmin">${Math.round(CONFIG.antiDetection.minBreakBetweenGames.min / 1000)}</span>
</div>
<input type="range" class="ba-slider" min="3" max="60" value="${Math.round(CONFIG.antiDetection.minBreakBetweenGames.min / 1000)}" id="slide-bgmin">
</div>
<div class="ba-section-title">Account & Behavior (v15.1)</div>
<div class="ba-row" title="Auto-detect new account and play -350 ELO for first 20 games, ramping up.">
<span class="ba-label">Warmup (auto)</span>
<div class="ba-checkbox ${CONFIG.warmup.enabled ? 'checked' : ''}" id="toggle-warmup"></div>
</div>
<div class="ba-row" title="Force warmup ON for fresh accounts regardless of game count.">
<span class="ba-label">Warmup (force ON)</span>
<div class="ba-checkbox ${CONFIG.warmup.manualOverride ? 'checked' : ''}" id="toggle-warmup-force"></div>
</div>
<div class="ba-slider-container" title="How many games the warmup ramp lasts.">
<div class="ba-slider-header">
<span class="ba-label">Warmup Games</span>
<span class="ba-value" id="val-warmup-games">${CONFIG.warmup.durationGames}</span>
</div>
<input type="range" class="ba-slider" min="5" max="50" value="${CONFIG.warmup.durationGames}" id="slide-warmup-games">
</div>
<div class="ba-row" title="Scale auto-lose probability based on long-window winrate to keep account around target %.">
<span class="ba-label">Winrate Targeting</span>
<div class="ba-checkbox ${CONFIG.winrateTarget.enabled ? 'checked' : ''}" id="toggle-wrtarget"></div>
</div>
<div class="ba-slider-container" title="Target winrate % the account should converge toward.">
<div class="ba-slider-header">
<span class="ba-label">Target Winrate %</span>
<span class="ba-value" id="val-wrtarget">${Math.round(CONFIG.winrateTarget.target * 100)}</span>
</div>
<input type="range" class="ba-slider" min="40" max="65" value="${Math.round(CONFIG.winrateTarget.target * 100)}" id="slide-wrtarget">
</div>
<div class="ba-row" title="Pick 1-2 openings per color per account and stick to them (real players have a repertoire).">
<span class="ba-label">Hard Repertoire</span>
<div class="ba-checkbox ${CONFIG.repertoireHard.enabled ? 'checked' : ''}" id="toggle-repertoire"></div>
</div>
<div class="ba-row" title="After a loss, next 2-4 games have higher blunder rate + slower timing (humans tilt).">
<span class="ba-label">Tilt After Loss</span>
<div class="ba-checkbox ${CONFIG.tilt.enabled ? 'checked' : ''}" id="toggle-tilt"></div>
</div>
<div class="ba-row" title="Only premove when opponent reply is forced or obvious recapture. Unchecked = premove whenever reply is predicted.">
<span class="ba-label">Smart Premove Gating</span>
<div class="ba-checkbox ${CONFIG.premoveGating.enabled ? 'checked' : ''}" id="toggle-pregate"></div>
</div>
<div class="ba-row" title="Refuse to auto-queue games of a different time control than the first one this session.">
<span class="ba-label">Time Control Lock</span>
<div class="ba-checkbox ${CONFIG.tcLock.enabled ? 'checked' : ''}" id="toggle-tclock"></div>
</div>
<div class="ba-row" title="Per-game random engine source (API / local / shallow local) so tactical signature varies.">
<span class="ba-label">Engine Rotation</span>
<div class="ba-checkbox ${CONFIG.engineRotation.enabled ? 'checked' : ''}" id="toggle-engrot"></div>
</div>
<div class="ba-row" title="Hesitate / blunder / sometimes hold lost positions instead of clean resigning.">
<span class="ba-label">Messy Resignation</span>
<div class="ba-checkbox ${CONFIG.messyResign.enabled ? 'checked' : ''}" id="toggle-messy"></div>
</div>
<div class="ba-row" title="Persistent per-account input device persona (mouse/trackpad/tablet) affecting drag feel.">
<span class="ba-label">Hardware Persona</span>
<div class="ba-checkbox ${CONFIG.hardwarePersona.enabled ? 'checked' : ''}" id="toggle-hwp"></div>
</div>
<div class="ba-row" title="Clear saved account state (game count, repertoire, hardware, recent results). Use when switching accounts.">
<span class="ba-label">Reset Account State</span>
<div class="ba-checkbox" id="btn-reset-account" style="background: rgba(255, 80, 80, 0.2); border-color: rgba(255, 80, 80, 0.6);"></div>
</div>
</div>
<!-- VISUALS TAB -->
<div id="tab-visuals" class="ba-page">
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Arrow Opacity</span>
<span class="ba-value" id="val-opacity">${Math.round(CONFIG.arrowOpacity * 100)}</span>
</div>
<input type="range" class="ba-slider" min="10" max="100" value="${Math.round(CONFIG.arrowOpacity * 100)}" id="slide-opacity">
</div>
<div class="ba-slider-container">
<div class="ba-slider-header">
<span class="ba-label">Drag Speed</span>
<span class="ba-value" id="val-drag-speed">${CONFIG.dragSpeed.toFixed(1)}</span>
</div>
<input type="range" class="ba-slider" min="2" max="20" value="${Math.round(CONFIG.dragSpeed * 10)}" id="slide-drag-speed">
</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>
<!-- STATS TAB -->
<div id="tab-stats" class="ba-page">
<div class="ba-section-title">Current Game</div>
<div class="ba-stats-row">
<span class="ba-stats-label">Engine Correlation</span>
<span class="ba-stats-value" id="stat-corr">---</span>
</div>
<div class="ba-stats-row">
<span class="ba-stats-label">Best Moves Played</span>
<span class="ba-stats-value" id="stat-best">0/0</span>
</div>
<div class="ba-stats-row">
<span class="ba-stats-label">Move Count</span>
<span class="ba-stats-value" id="stat-moves">0</span>
</div>
<div class="ba-stats-row">
<span class="ba-stats-label">Game Phase</span>
<span class="ba-stats-value" id="stat-phase">---</span>
</div>
<div class="ba-stats-row">
<span class="ba-stats-label">Cluster Mode</span>
<span class="ba-stats-value" id="stat-cluster">normal</span>
</div>
<div class="ba-stats-row">
<span class="ba-stats-label">Time Pressure</span>
<span class="ba-stats-value" id="stat-tp">---</span>
</div>
<div class="ba-stats-row">
<span class="ba-stats-label">Opponent Rating</span>
<span class="ba-stats-value" id="stat-opp-rating">---</span>
</div>
<div class="ba-section-title">Session</div>
<div class="ba-stats-row">
<span class="ba-stats-label">Games Played</span>
<span class="ba-stats-value" id="stat-games">${CONFIG.session.gamesPlayed}</span>
</div>
<div class="ba-stats-row">
<span class="ba-stats-label">Win Streak</span>
<span class="ba-stats-value" id="stat-streak">${CONFIG.session.currentWinStreak}</span>
</div>
<div class="ba-stats-row">
<span class="ba-stats-label">Engine Source</span>
<span class="ba-stats-value" id="stat-engine">---</span>
</div>
<div class="ba-stats-row">
<span class="ba-stats-label">API Status</span>
<span class="ba-stats-value" id="stat-api">checking...</span>
</div>
<div class="ba-stats-row">
<span class="ba-stats-label">My Clock</span>
<span class="ba-stats-value" id="stat-clock">---</span>
</div>
</div>
</div>
<div class="ba-footer">
<span><span class="ba-key">A</span> Auto</span>
<span><span class="ba-key">X</span> Stealth</span>
<span><span class="ba-key">R</span> Reset</span>
</div>
`;
document.body.appendChild(panel);
State.ui.panel = panel;
UI.makeDraggable(panel);
UI.initListeners(panel);
},
initListeners: (panel) => {
// Tab switching
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');
});
});
// Minimize
panel.querySelector('.ba-minimize').addEventListener('click', () => {
panel.classList.toggle('ba-minimized');
});
// Checkbox toggles
const toggle = (id, getter, setter) => {
const el = panel.querySelector(`#${id}`);
if (!el) return;
el.addEventListener('click', (e) => {
const newVal = !getter();
setter(newVal);
e.target.classList.toggle('checked', newVal);
Settings.save(CONFIG);
});
};
toggle('toggle-auto', () => CONFIG.auto.enabled, (v) => CONFIG.auto.enabled = v);
toggle('toggle-book', () => CONFIG.useBook, (v) => CONFIG.useBook = v);
toggle('toggle-tablebase', () => CONFIG.useTablebase, (v) => CONFIG.useTablebase = v);
toggle('toggle-threats', () => CONFIG.showThreats, (v) => CONFIG.showThreats = v);
toggle('toggle-dynamic-depth', () => CONFIG.engineDepth.dynamicDepth, (v) => CONFIG.engineDepth.dynamicDepth = v);
toggle('toggle-clock-aware', () => CONFIG.timing.clockAware.enabled, (v) => CONFIG.timing.clockAware.enabled = v);
toggle('toggle-premove', () => CONFIG.timing.premove.enabled, (v) => CONFIG.timing.premove.enabled = v);
// SAFETY tab toggles
toggle('toggle-random-afk', () => CONFIG.antiDetection.randomAFK.enabled, (v) => CONFIG.antiDetection.randomAFK.enabled = v);
toggle('toggle-auto-queue', () => CONFIG.auto.autoQueue, (v) => CONFIG.auto.autoQueue = v);
toggle('toggle-auto-lose', () => CONFIG.autoLose.enabled, (v) => CONFIG.autoLose.enabled = v);
toggle('toggle-auto-resign', () => CONFIG.autoResign.enabled, (v) => CONFIG.autoResign.enabled = v);
toggle('toggle-opp-adapt', () => CONFIG.opponentAdaptation.enabled, (v) => CONFIG.opponentAdaptation.enabled = v);
toggle('toggle-time-pressure', () => CONFIG.timePressure.enabled, (v) => CONFIG.timePressure.enabled = v);
// New: advanced humanization toggles
toggle('toggle-human-master', () => CONFIG.humanization.enabled, (v) => CONFIG.humanization.enabled = v);
toggle('toggle-streaks', () => CONFIG.humanization.streaks.enabled, (v) => CONFIG.humanization.streaks.enabled = v);
toggle('toggle-clustering', () => CONFIG.humanization.accuracyClustering.enabled, (v) => CONFIG.humanization.accuracyClustering.enabled = v);
toggle('toggle-weakness', () => CONFIG.humanization.weaknessProfile.enabled, (v) => CONFIG.humanization.weaknessProfile.enabled = v);
toggle('toggle-playerdb', () => CONFIG.humanization.playerMoveDB.enabled, (v) => CONFIG.humanization.playerMoveDB.enabled = v);
toggle('toggle-timing-couple', () => CONFIG.humanization.timingAccuracyCoupling.enabled,(v) => CONFIG.humanization.timingAccuracyCoupling.enabled = v);
toggle('toggle-cofmind', () => CONFIG.antiDetection.changeOfMind.enabled, (v) => CONFIG.antiDetection.changeOfMind.enabled = v);
toggle('toggle-telemetry', () => CONFIG.antiDetection.telemetryNoise.enabled, (v) => CONFIG.antiDetection.telemetryNoise.enabled = v);
// v15.1: Account & behavior toggles
toggle('toggle-warmup', () => CONFIG.warmup.enabled, (v) => CONFIG.warmup.enabled = v);
toggle('toggle-warmup-force', () => CONFIG.warmup.manualOverride, (v) => CONFIG.warmup.manualOverride = v);
toggle('toggle-wrtarget', () => CONFIG.winrateTarget.enabled, (v) => CONFIG.winrateTarget.enabled = v);
toggle('toggle-repertoire', () => CONFIG.repertoireHard.enabled, (v) => CONFIG.repertoireHard.enabled = v);
toggle('toggle-tilt', () => CONFIG.tilt.enabled, (v) => CONFIG.tilt.enabled = v);
toggle('toggle-pregate', () => CONFIG.premoveGating.enabled, (v) => CONFIG.premoveGating.enabled = v);
toggle('toggle-tclock', () => CONFIG.tcLock.enabled, (v) => CONFIG.tcLock.enabled = v);
toggle('toggle-engrot', () => CONFIG.engineRotation.enabled, (v) => CONFIG.engineRotation.enabled = v);
toggle('toggle-messy', () => CONFIG.messyResign.enabled, (v) => CONFIG.messyResign.enabled = v);
toggle('toggle-hwp', () => CONFIG.hardwarePersona.enabled, (v) => CONFIG.hardwarePersona.enabled = v);
// Account reset button (not a toggle — behaves as one-shot action)
const resetBtn = panel.querySelector('#btn-reset-account');
if (resetBtn) {
resetBtn.addEventListener('click', () => {
if (!confirm('Reset account state?\n\nThis clears: total games played, chosen repertoire, hardware persona, recent results window, locked session TC.\n\nYou should do this when switching to a different Chess.com account.')) return;
CONFIG.account = {
totalGamesPlayed: 0,
repertoire: { white: null, black: null },
hardware: null,
recentResults: [],
sessionTC: null,
currentEngine: null,
};
Account._tiltGamesLeft = 0;
State.gameEngineOverride = null;
Settings.save(CONFIG);
UI.toast('Account Reset', 'Account state cleared — repertoire and hardware will be re-chosen next game', 'info', 4500);
Utils.log('Account state reset by user', 'warn');
});
}
// Sliders
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;
Settings.save(CONFIG);
});
};
slider('slide-rating', 'val-rating', (v) => {
CONFIG.targetRating = v;
RatingProfile.apply(v);
// Update other sliders to reflect new values
UI.refreshSliders();
});
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; });
slider('slide-depth', 'val-depth', (v) => { CONFIG.engineDepth.base = v; });
slider('slide-mpv', 'val-mpv', (v) => {
CONFIG.multiPV = v;
// Update local engine MultiPV
if (State.workers.stockfish) {
State.workers.stockfish.postMessage(`setoption name MultiPV value ${v}`);
}
});
slider('slide-opacity', 'val-opacity', (v) => { CONFIG.arrowOpacity = v / 100; });
// Drag speed: slider 2-20 maps to 0.2-2.0
const dragSlider = panel.querySelector('#slide-drag-speed');
const dragVal = panel.querySelector('#val-drag-speed');
if (dragSlider) {
dragSlider.addEventListener('input', (e) => {
const v = parseInt(e.target.value) / 10;
CONFIG.dragSpeed = v;
if (dragVal) dragVal.textContent = v.toFixed(1);
Settings.save(CONFIG);
});
}
// SAFETY tab sliders
slider('slide-afk-chance', 'val-afk-chance', (v) => { CONFIG.antiDetection.randomAFK.chance = v / 100; });
slider('slide-random-legal', 'val-random-legal', (v) => { CONFIG.antiDetection.randomLegalMoveChance = v / 100; });
slider('slide-max-gph', 'val-max-gph', (v) => { CONFIG.antiDetection.maxGamesPerHour = v; });
slider('slide-max-session', 'val-max-session', (v) => { CONFIG.session.maxGamesPerSession = v; });
slider('slide-break-dur', 'val-break-dur', (v) => { CONFIG.session.breakDurationMs = v * 60000; });
slider('slide-lose-streak', 'val-lose-streak', (v) => { CONFIG.autoLose.triggerStreak = v; });
slider('slide-max-streak', 'val-max-streak', (v) => { CONFIG.session.maxWinStreak = v; });
slider('slide-rating-edge', 'val-rating-edge', (v) => { CONFIG.opponentAdaptation.ratingEdge = v; });
slider('slide-resign-chance', 'val-resign-chance', (v) => { CONFIG.autoResign.resignChance = v / 100; });
// New: advanced humanization sliders
slider('slide-topcap', 'val-topcap', (v) => { CONFIG.humanization.antiCorrelation.maxTopMoveRate = v / 100; });
slider('slide-sjitter', 'val-sjitter', (v) => { CONFIG.antiDetection.sessionLengthJitter = v / 100; });
slider('slide-bgmin', 'val-bgmin', (v) => {
CONFIG.antiDetection.minBreakBetweenGames.min = v * 1000;
// Keep max >= min + 5s
if (CONFIG.antiDetection.minBreakBetweenGames.max < (v + 5) * 1000) {
CONFIG.antiDetection.minBreakBetweenGames.max = (v + 15) * 1000;
}
});
// v15.1 account sliders
slider('slide-warmup-games', 'val-warmup-games', (v) => { CONFIG.warmup.durationGames = v; });
slider('slide-wrtarget', 'val-wrtarget', (v) => { CONFIG.winrateTarget.target = v / 100; });
// Float sliders (resign eval, blunder chance)
const floatSlider = (id, valId, setter) => {
const el = panel.querySelector(`#${id}`);
const display = panel.querySelector(`#${valId}`);
if (!el || !display) return;
el.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
setter(val);
display.textContent = val;
Settings.save(CONFIG);
});
};
floatSlider('slide-resign-eval', 'val-resign-eval', (v) => { CONFIG.autoResign.evalThreshold = v; });
floatSlider('slide-blunder', 'val-blunder', (v) => { CONFIG.humanization.blunder.chance = v / 100; });
// Selects
const engineSelect = panel.querySelector('#select-engine');
if (engineSelect) {
engineSelect.addEventListener('change', (e) => {
CONFIG.engineType = e.target.value;
Utils.log(`Engine changed to: ${e.target.value}`);
Settings.save(CONFIG);
});
}
const styleSelect = panel.querySelector('#select-style');
if (styleSelect) {
styleSelect.addEventListener('change', (e) => {
CONFIG.playStyle = e.target.value;
RatingProfile.apply(CONFIG.targetRating);
UI.refreshSliders();
UI.setAccent();
});
}
UI.setAccent();
},
refreshSliders: () => {
const panel = State.ui.panel;
if (!panel) return;
const updates = {
'slide-corr': { val: 'val-corr', v: Math.round(CONFIG.humanization.targetEngineCorrelation * 100) },
'slide-min': { val: 'val-min', v: CONFIG.timing.base.min },
'slide-max': { val: 'val-max', v: CONFIG.timing.base.max },
'slide-subopt': { val: 'val-subopt', v: Math.round(CONFIG.humanization.suboptimalMoveRate.middlegame * 100) },
'slide-depth': { val: 'val-depth', v: CONFIG.engineDepth.base },
// SAFETY tab
'slide-afk-chance': { val: 'val-afk-chance', v: Math.round(CONFIG.antiDetection.randomAFK.chance * 100) },
'slide-random-legal': { val: 'val-random-legal', v: Math.round(CONFIG.antiDetection.randomLegalMoveChance * 100) },
'slide-max-gph': { val: 'val-max-gph', v: CONFIG.antiDetection.maxGamesPerHour },
'slide-max-session': { val: 'val-max-session', v: CONFIG.session.maxGamesPerSession },
'slide-break-dur': { val: 'val-break-dur', v: Math.round(CONFIG.session.breakDurationMs / 60000) },
'slide-lose-streak': { val: 'val-lose-streak', v: CONFIG.autoLose.triggerStreak },
'slide-max-streak': { val: 'val-max-streak', v: CONFIG.session.maxWinStreak },
'slide-rating-edge': { val: 'val-rating-edge', v: CONFIG.opponentAdaptation.ratingEdge },
'slide-resign-chance': { val: 'val-resign-chance', v: Math.round(CONFIG.autoResign.resignChance * 100) },
'slide-resign-eval': { val: 'val-resign-eval', v: CONFIG.autoResign.evalThreshold },
'slide-blunder': { val: 'val-blunder', v: (CONFIG.humanization.blunder.chance * 100).toFixed(1) },
// New advanced sliders
'slide-topcap': { val: 'val-topcap', v: Math.round(CONFIG.humanization.antiCorrelation.maxTopMoveRate * 100) },
'slide-sjitter': { val: 'val-sjitter', v: Math.round(CONFIG.antiDetection.sessionLengthJitter * 100) },
'slide-bgmin': { val: 'val-bgmin', v: Math.round(CONFIG.antiDetection.minBreakBetweenGames.min / 1000) },
// v15.1
'slide-warmup-games': { val: 'val-warmup-games', v: CONFIG.warmup.durationGames },
'slide-wrtarget': { val: 'val-wrtarget', v: Math.round(CONFIG.winrateTarget.target * 100) },
};
for (const [slideId, info] of Object.entries(updates)) {
const slider = panel.querySelector(`#${slideId}`);
const display = panel.querySelector(`#${info.val}`);
if (slider) slider.value = info.v;
if (display) display.textContent = info.v;
}
},
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;
el.style.left = `${initialLeft + (e.clientX - startX)}px`;
el.style.top = `${initialTop + (e.clientY - startY)}px`;
});
document.addEventListener('mouseup', () => {
isDragging = false;
header.style.cursor = 'grab';
});
},
toggleStealth: () => {
CONFIG.stealthMode = !CONFIG.stealthMode;
const p = State.ui.panel;
if (p) p.style.display = CONFIG.stealthMode ? 'none' : 'flex';
document.querySelectorAll('.ba-overlay').forEach(el => {
el.classList.toggle('ba-stealth', CONFIG.stealthMode);
});
if (UI._toastContainer) {
if (CONFIG.stealthMode) {
UI._toastContainer.innerHTML = '';
UI._toastContainer.style.display = 'none';
} else {
UI._toastContainer.style.display = 'flex';
}
}
},
updatePanel: (evalData, bestMove) => {
if (!State.ui.panel) return;
const evalBox = State.ui.panel.querySelector('#eval-display');
const moveBox = State.ui.panel.querySelector('#move-display');
const badge = State.ui.panel.querySelector('#engine-badge');
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 || '...';
}
// Update engine badge
const engineNames = {
'api': 'chess-api.com SF18',
'stockfish_online': 'SF Online',
'local': 'Local SF10',
'tablebase': 'Syzygy TB',
'book': 'Opening Book',
};
if (badge && State._lastEngineSource) {
badge.textContent = engineNames[State._lastEngineSource] || State._lastEngineSource;
}
// Update stats tab
UI.updateStats();
State.ui.panel.querySelector('#toggle-auto').classList.toggle('checked', CONFIG.auto.enabled);
},
updateStats: () => {
const panel = State.ui.panel;
if (!panel) return;
const h = State.human;
const corr = h.totalMoveCount > 0 ? `${((h.bestMoveCount / h.totalMoveCount) * 100).toFixed(0)}%` : '---';
const setVal = (id, val) => {
const el = panel.querySelector(`#${id}`);
if (el) el.textContent = val;
};
setVal('stat-corr', corr);
setVal('stat-best', `${h.bestMoveCount}/${h.totalMoveCount}`);
setVal('stat-moves', State.moveCount);
setVal('stat-phase', HumanStrategy.getGamePhase(State.lastFen));
setVal('stat-cluster', h.clusterMode);
// Time pressure indicator
const tpMult = h.timePressureMult?.suboptimal || 1;
setVal('stat-tp', tpMult > 1 ? `ACTIVE x${tpMult.toFixed(1)}` : 'normal');
// Opponent rating
setVal('stat-opp-rating', h.opponentRating ? `${h.opponentRating}` : '---');
setVal('stat-games', CONFIG.session.gamesPlayed);
const streakText = h.autoLoseActive
? `${CONFIG.session.currentWinStreak} [AUTO-LOSE]`
: `${CONFIG.session.currentWinStreak}`;
setVal('stat-streak', streakText);
setVal('stat-engine', State.gameEngineOverride || CONFIG.engineType);
setVal('stat-api', State.apiAvailable ? 'OK' : `DOWN (${State.apiFailCount} fails)`);
setVal('stat-clock', State.clock.myTime != null ? `${State.clock.myTime}s` : '---');
},
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;
},
_toastContainer: null,
_toastIcons: {
info: '\u{1F4A1}', warn: '\u26A0\uFE0F', error: '\u{1F6A8}', success: '\u2705', move: '\u265E',
blunder: '\u{1F4A5}', suboptimal: '\u{1F504}', book: '\u{1F4D6}', resign: '\u{1F3F3}\uFE0F',
afk: '\u{1F634}', fakeout: '\u{1F3AD}', streak: '\u{1F525}', engine: '\u2699\uFE0F',
tablebase: '\u{1F4BE}', draw: '\u{1F91D}', queue: '\u{1F504}', time: '\u23F1\uFE0F',
},
toast: (title, msg, type = 'info', durationMs = 4000) => {
if (CONFIG.stealthMode) return;
if (!UI._toastContainer) {
UI._toastContainer = document.createElement('div');
UI._toastContainer.className = 'ba-toast-container';
document.body.appendChild(UI._toastContainer);
}
const typeClass = ['info','warn','error','success','move'].includes(type) ? type : 'info';
const icon = UI._toastIcons[type] || UI._toastIcons[typeClass] || UI._toastIcons.info;
const el = document.createElement('div');
el.className = `ba-toast ba-toast-${typeClass}`;
el.innerHTML = `
<span class="ba-toast-icon">${icon}</span>
<div class="ba-toast-body">
<div class="ba-toast-title">${title}</div>
<div class="ba-toast-msg">${msg}</div>
</div>
<div class="ba-toast-progress" style="animation-duration:${durationMs}ms"></div>
`;
el.addEventListener('click', () => {
el.style.animation = 'ba-toast-out 0.25s ease-in forwards';
setTimeout(() => el.remove(), 250);
});
UI._toastContainer.appendChild(el);
// Cap at 5 visible toasts
while (UI._toastContainer.children.length > 5) {
UI._toastContainer.firstChild.remove();
}
setTimeout(() => {
if (el.parentNode) {
el.style.animation = 'ba-toast-out 0.25s ease-in forwards';
setTimeout(() => el.remove(), 250);
}
}, durationMs);
},
clearOverlay: () => {
document.querySelectorAll('.ba-overlay').forEach(e => e.remove());
},
_arrowId: 0,
drawMove: (move, color = '#4caf50', secondary = false) => {
if (CONFIG.stealthMode || !move || move.length < 4) 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), to = move.substring(2, 4);
const fl = (c) => c.charCodeAt(0) - 97, rk = (c) => parseInt(c) - 1;
const flipped = State.playerColor === 'b';
const sz = rect.width / 8;
const pos = (sq) => ({
x: (flipped ? 7 - fl(sq[0]) : fl(sq[0])) * sz + sz / 2,
y: (flipped ? rk(sq[1]) : 7 - rk(sq[1])) * sz + sz / 2
});
const p1 = pos(from), p2 = pos(to);
const ns = 'http://www.w3.org/2000/svg';
let svg = overlay.querySelector('svg');
if (!svg) {
svg = document.createElementNS(ns, 'svg');
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.setAttribute('viewBox', `0 0 ${rect.width} ${rect.height}`);
overlay.appendChild(svg);
}
let defs = svg.querySelector('defs');
if (!defs) { defs = document.createElementNS(ns, 'defs'); svg.prepend(defs); }
const id = ++UI._arrowId;
const opacity = CONFIG.arrowOpacity * (secondary ? 0.55 : 1);
const w = sz * (secondary ? 0.065 : 0.12);
const headLen = sz * (secondary ? 0.18 : 0.32);
const headW = w * (secondary ? 2.8 : 3.2);
const dx = p2.x - p1.x, dy = p2.y - p1.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const ux = dx / dist, uy = dy / dist;
const shorten = headLen * 0.7;
const ex = p2.x - ux * shorten, ey = p2.y - uy * shorten;
const grp = document.createElementNS(ns, 'g');
grp.setAttribute('class', 'ba-arrow-group');
grp.setAttribute('opacity', opacity);
const marker = document.createElementNS(ns, 'marker');
marker.setAttribute('id', `ah${id}`);
marker.setAttribute('markerWidth', headLen);
marker.setAttribute('markerHeight', headW);
marker.setAttribute('refX', headLen - 1);
marker.setAttribute('refY', headW / 2);
marker.setAttribute('orient', 'auto');
marker.setAttribute('markerUnits', 'userSpaceOnUse');
const arrow = document.createElementNS(ns, 'path');
arrow.setAttribute('d', `M0,${headW * 0.15} L${headLen},${headW / 2} L0,${headW * 0.85} Z`);
arrow.setAttribute('fill', color);
if (!secondary) arrow.setAttribute('filter', 'url(#aglow)');
marker.appendChild(arrow);
defs.appendChild(marker);
if (!defs.querySelector('#aglow')) {
const filt = document.createElementNS(ns, 'filter');
filt.setAttribute('id', 'aglow');
filt.setAttribute('x', '-50%'); filt.setAttribute('y', '-50%');
filt.setAttribute('width', '200%'); filt.setAttribute('height', '200%');
const blur = document.createElementNS(ns, 'feGaussianBlur');
blur.setAttribute('stdDeviation', '2.5');
blur.setAttribute('result', 'glow');
const merge = document.createElementNS(ns, 'feMerge');
const mn1 = document.createElementNS(ns, 'feMergeNode'); mn1.setAttribute('in', 'glow');
const mn2 = document.createElementNS(ns, 'feMergeNode'); mn2.setAttribute('in', 'SourceGraphic');
merge.appendChild(mn1); merge.appendChild(mn2);
filt.appendChild(blur); filt.appendChild(merge);
defs.appendChild(filt);
}
if (!secondary) {
const sqX = (flipped ? 7 - fl(from[0]) : fl(from[0])) * sz;
const sqY = (flipped ? rk(from[1]) : 7 - rk(from[1])) * sz;
const highlight = document.createElementNS(ns, 'rect');
highlight.setAttribute('x', sqX); highlight.setAttribute('y', sqY);
highlight.setAttribute('width', sz); highlight.setAttribute('height', sz);
highlight.setAttribute('rx', sz * 0.06);
highlight.setAttribute('fill', color);
highlight.setAttribute('opacity', '0.18');
grp.appendChild(highlight);
}
const line = document.createElementNS(ns, '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', w);
line.setAttribute('stroke-linecap', 'round');
line.setAttribute('marker-end', `url(#ah${id})`);
if (!secondary) {
line.setAttribute('filter', 'url(#aglow)');
} else {
line.setAttribute('stroke-dasharray', `${w * 1.5} ${w * 2.5}`);
line.setAttribute('class', 'ba-dash-anim');
}
grp.appendChild(line);
const ring = document.createElementNS(ns, 'circle');
ring.setAttribute('cx', p2.x); ring.setAttribute('cy', p2.y);
ring.setAttribute('r', sz * (secondary ? 0.15 : 0.22));
ring.setAttribute('fill', 'none');
ring.setAttribute('stroke', color);
ring.setAttribute('stroke-width', w * 0.6);
ring.setAttribute('class', 'ba-pulse');
grp.appendChild(ring);
if (!secondary) {
const dot = document.createElementNS(ns, 'circle');
dot.setAttribute('cx', p2.x); dot.setAttribute('cy', p2.y);
dot.setAttribute('r', sz * 0.06);
dot.setAttribute('fill', color);
dot.setAttribute('opacity', '0.9');
grp.appendChild(dot);
}
svg.appendChild(grp);
}
};
// ═══════════════════════════════════════════
// GAME INTERFACE
// ═══════════════════════════════════════════
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;
},
getBoardGame: () => {
const b = Game.getBoard();
if (!b) return null;
if (b.game) return b.game;
try { const ub = unsafeWindow.document.querySelector('wc-chess-board'); if (ub && ub.game) return ub.game; } catch (e) {}
return null;
},
squareToCoords: (sq) => {
const file = sq.charCodeAt(0) - 96;
const rank = sq[1];
return `${file}${rank}`;
},
detectColor: () => {
const g = Game.getBoardGame();
if (g) {
try { if (g.getOptions) { const o = g.getOptions(); if (o && typeof o.flipped === 'boolean') return o.flipped ? 'b' : 'w'; if (o && typeof o.isWhiteOnBottom === 'boolean') return o.isWhiteOnBottom ? 'w' : 'b'; } } catch (e) {}
try { if (g.getPlayingAs) { const pa = g.getPlayingAs(); if (pa === 1) return 'w'; if (pa === 2) return 'b'; } } catch (e) {}
}
const b = Game.getBoard();
if (b) {
try { if (typeof b.isFlipped === 'function') return b.isFlipped() ? 'b' : 'w'; } catch (e) {}
}
Utils.log('detectColor: fallback w', 'warn');
return 'w';
},
isValidFen: (fen) => {
if (!fen || typeof fen !== 'string') return false;
return fen.split(' ').length >= 4;
},
getFen: () => {
const g = Game.getBoardGame();
if (g) {
try { if (g.getFEN) return g.getFEN(); } catch (e) {}
}
const board = Game.getBoard();
if (!board) return null;
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;
return fen.split(' ')[1] === 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}`);
},
// Read clock time from the DOM
readClock: () => {
try {
const clocks = document.querySelectorAll('.clock-component, .clock-time-monospace');
if (clocks.length < 2) return;
// Determine which clock is ours based on position
// Bottom clock is always the current player's
const clockEls = Array.from(clocks);
const sorted = clockEls.sort((a, b) => {
const ra = a.getBoundingClientRect();
const rb = b.getBoundingClientRect();
return rb.top - ra.top; // bottom first
});
const parseTime = (el) => {
const text = el.textContent.trim().replace(/[^\d:\.]/g, '');
const parts = text.split(':').map(Number);
if (parts.some(isNaN)) return null;
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
if (parts.length === 2) return parts[0] * 60 + parts[1];
if (parts.length === 1) return parts[0];
return null;
};
State.clock.myTime = parseTime(sorted[0]);
State.clock.oppTime = parseTime(sorted[1]);
} catch (e) { /* ignore clock read errors */ }
},
// Read opponent's rating from the page.
// Chess.com's DOM has shifted multiple times; this tries every selector
// pattern seen across recent UI versions, then falls back to a generic
// "find a 3-4 digit number near the top player area" heuristic.
readOpponentRating: () => {
try {
const parseRating = (raw) => {
if (!raw) return null;
const txt = String(raw).replace(/[(),\s]/g, '');
const m = txt.match(/(\d{3,4})/);
if (!m) return null;
const n = parseInt(m[1], 10);
return (n >= 100 && n <= 4000) ? n : null;
};
// Targeted selectors covering live, daily, computer and bullet UIs
const selectors = [
// Current "user-tagline" (live)
'.player-component.player-top .user-tagline-rating',
'.player-row-top .cc-user-rating',
'.player-top .cc-user-rating',
'.board-layout-top .user-tagline-rating',
'.board-player-default-top .user-tagline-rating',
'.player-top .rating-tagline',
// V5 / new live game UI
'[data-test-element="user-tagline-username"] ~ .user-tagline-rating',
'.cc-user-tagline-component-top .cc-user-rating',
// Generic fallback: any rating element inside any "top" container
'[class*="top"] [class*="rating"]',
];
for (const sel of selectors) {
const els = document.querySelectorAll(sel);
for (const el of els) {
const r = parseRating(el.textContent);
if (r) return r;
}
}
// Heuristic fallback: every visible element that holds a 3-4 digit
// rating. We pick the topmost one geometrically (board isn't flipped
// ⇒ opponent is on top of viewport).
const candidates = [];
document.querySelectorAll('span, div').forEach((el) => {
if (el.children.length > 0) return; // leaf nodes only
const cls = el.className || '';
if (typeof cls !== 'string') return;
if (!/rating|tagline|user/i.test(cls)) return;
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const r = parseRating(el.textContent);
if (r) candidates.push({ rating: r, top: rect.top });
});
if (candidates.length >= 2) {
candidates.sort((a, b) => a.top - b.top);
return candidates[0].rating;
}
if (candidates.length === 1) return candidates[0].rating;
} catch (e) { /* ignore */ }
return null;
},
// Resign the current game
_clickConfirmResign: async () => {
// Wait for the confirmation dialog to appear, retrying a few times
for (let attempt = 0; attempt < 10; attempt++) {
await Utils.sleep(300);
// Selector-based matches
const selectorCandidates = [
'button[data-cy="confirm-resign-button"]',
'.resign-confirm-button',
'.modal-confirm-button',
'.ui_v5-button-primary',
'.modal-seo-close-button',
];
for (const sel of selectorCandidates) {
const btn = document.querySelector(sel);
if (btn && btn.offsetParent !== null) {
btn.click();
Utils.log('Auto-Resign: Confirmed via selector', 'info');
return true;
}
}
// Text-based match: find any visible button containing "Yes" or "Resign"
const allButtons = document.querySelectorAll('button, [role="button"]');
for (const btn of allButtons) {
if (btn.offsetParent === null) continue;
const text = btn.textContent.trim().toLowerCase();
if (text === 'yes' || text === 'resign' || text === 'confirm') {
btn.click();
Utils.log(`Auto-Resign: Confirmed via text match ("${btn.textContent.trim()}")`, 'info');
return true;
}
}
}
Utils.log('Auto-Resign: Confirmation dialog not found', 'warn');
return false;
},
resign: async () => {
try {
Utils.log('Auto-Resign: Attempting to resign...', 'warn');
const resignSelectors = [
'button[data-cy="resign-button"]',
'.resign-button-component',
'[data-tooltip="Resign"]',
'.board-controls-btn-resign',
];
for (const sel of resignSelectors) {
const btn = document.querySelector(sel);
if (btn && btn.offsetParent !== null) {
btn.click();
const confirmed = await Game._clickConfirmResign();
if (confirmed) return true;
return true;
}
}
// Text-based fallback: find any button/icon whose text or tooltip says "Resign"
const allBtns = document.querySelectorAll('button, [role="button"], .board-controls-btn');
for (const btn of allBtns) {
if (btn.offsetParent === null) continue;
const text = (btn.textContent + ' ' + (btn.getAttribute('data-tooltip') || '') + ' ' + (btn.getAttribute('aria-label') || '')).toLowerCase();
if (text.includes('resign')) {
btn.click();
const confirmed = await Game._clickConfirmResign();
if (confirmed) return true;
return true;
}
}
// Fallback: try the game menu
const menuBtn = document.querySelector('.board-controls-btn-menu, [data-cy="game-controls-menu"]');
if (menuBtn) {
menuBtn.click();
await Utils.sleep(500);
const resignItem = Array.from(document.querySelectorAll('.board-controls-menu-item, [class*="menu-item"]'))
.find(el => el.textContent.toLowerCase().includes('resign'));
if (resignItem) {
resignItem.click();
const confirmed = await Game._clickConfirmResign();
if (confirmed) return true;
return true;
}
}
Utils.log('Auto-Resign: Could not find resign button', 'warn');
return false;
} catch (e) {
Utils.log('Auto-Resign: Error - ' + e, 'error');
return false;
}
},
};
// ═══════════════════════════════════════════
// OPENING BOOK
// ═══════════════════════════════════════════
const OpeningBook = {
_repertoire: { w: {}, b: {} }, // track chosen lines for consistency
fetchMove: (fen) => {
if (!CONFIG.useBook) return Promise.resolve(null);
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://explorer.lichess.ovh/masters?fen=${encodeURIComponent(fen)}`,
timeout: 4000,
onload: (response) => {
try {
const data = JSON.parse(response.responseText);
if (data.moves && data.moves.length > 0) {
const color = State.playerColor || 'w';
const posKey = fen.split(' ').slice(0, 4).join(' ');
// Repertoire consistency: if we've played this position before, prefer same move
if (CONFIG.humanization.repertoireConsistency?.enabled && OpeningBook._repertoire[color][posKey]) {
const prevMove = OpeningBook._repertoire[color][posKey];
const found = data.moves.find(m => m.uci === prevMove);
if (found) {
Utils.log(`Book: Repertoire hit - playing ${prevMove} again`, 'debug');
resolve(prevMove);
return;
}
}
// Weighted selection from top 3
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;
let chosen = topMoves[0].uci;
for (const move of topMoves) {
const games = move.white + move.draw + move.black;
if (r < games) {
chosen = move.uci;
break;
}
r -= games;
}
// Remember this choice for repertoire consistency
OpeningBook._repertoire[color][posKey] = chosen;
resolve(chosen);
} else {
resolve(null);
}
} catch (e) {
resolve(null);
}
},
onerror: () => resolve(null),
ontimeout: () => resolve(null),
});
});
}
};
// ═══════════════════════════════════════════
// SYZYGY TABLEBASE
// ═══════════════════════════════════════════
const Tablebase = {
probe: (fen) => {
if (!CONFIG.useTablebase) return Promise.resolve(null);
const pieceCount = Utils.countPieces(fen);
if (pieceCount > CONFIG.tablebase.maxPieces) return Promise.resolve(null);
Utils.log(`Tablebase: Probing ${pieceCount}-piece position`, 'debug');
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: `${CONFIG.tablebase.url}?fen=${encodeURIComponent(fen)}`,
timeout: 5000,
onload: (response) => {
try {
const data = JSON.parse(response.responseText);
if (data.moves && data.moves.length > 0) {
// Sort by DTZ: winning moves first (positive DTZ for side to move)
// category: "win", "draw", "loss", etc.
const winning = data.moves.filter(m => m.category === 'win');
const drawing = data.moves.filter(m => m.category === 'draw' || m.category === 'blessed-loss');
const best = winning.length > 0 ? winning : (drawing.length > 0 ? drawing : data.moves);
// Don't always pick the absolute best DTZ - humans don't know tablebase
// Pick from top 3 winning moves for variety
const candidates = best.slice(0, 3);
const chosen = candidates[Math.floor(Math.random() * candidates.length)];
Utils.log(`Tablebase: ${chosen.uci} (${chosen.category}, DTZ: ${chosen.dtz})`, 'info');
resolve({ move: chosen.uci, category: chosen.category, dtz: chosen.dtz });
} else {
resolve(null);
}
} catch (e) {
resolve(null);
}
},
onerror: () => resolve(null),
ontimeout: () => resolve(null),
});
});
}
};
// ═══════════════════════════════════════════
// API ENGINE (chess-api.com / stockfish.online)
// ═══════════════════════════════════════════
const APIEngine = {
// chess-api.com (Stockfish 18.1)
analyzeChessApi: (fen, depth) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: CONFIG.api.chessApi.url,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({
fen: fen,
depth: Math.min(depth, 18),
maxThinkingTime: CONFIG.api.chessApi.maxThinkingTime,
}),
timeout: CONFIG.api.chessApi.timeout,
onload: (response) => {
try {
const data = JSON.parse(response.responseText);
if (data.move || data.text) {
let move = data.move;
if (!move && data.text) {
// Parse "bestmove e2e4 ponder d7d5"
const match = data.text.match(/bestmove\s+(\S+)/);
if (match) move = match[1];
}
if (!move) { reject('No move in response'); return; }
let evalValue = 0;
let evalType = 'cp';
if (data.mate != null && data.mate !== false) {
evalType = 'mate';
evalValue = data.mate;
} else if (data.eval != null) {
evalValue = typeof data.eval === 'number' ? data.eval / 100 : parseFloat(data.eval) / 100;
}
// Flip eval for black (API usually gives from white's perspective)
if (State.playerColor === 'b' && evalType === 'cp') {
evalValue = -evalValue;
}
resolve({
move: move,
eval: { type: evalType, value: evalValue },
depth: data.depth || depth,
ponder: data.ponder || null,
continuation: data.continuationArr || (data.continuation ? data.continuation.split(' ') : []),
source: 'api',
});
} else {
reject('Invalid API response');
}
} catch (e) {
reject(e);
}
},
onerror: (e) => reject(e),
ontimeout: () => reject('timeout'),
});
});
},
// stockfish.online (Stockfish 16)
analyzeStockfishOnline: (fen, depth) => {
return new Promise((resolve, reject) => {
const url = `${CONFIG.api.stockfishOnline.url}?fen=${encodeURIComponent(fen)}&depth=${Math.min(depth, 15)}`;
GM_xmlhttpRequest({
method: 'GET',
url: url,
timeout: CONFIG.api.stockfishOnline.timeout,
onload: (response) => {
try {
const data = JSON.parse(response.responseText);
if (data.success && data.bestmove) {
const moveMatch = data.bestmove.match(/bestmove\s+(\S+)/);
if (!moveMatch) { reject('No move parsed'); return; }
let evalValue = data.evaluation || 0;
let evalType = 'cp';
if (data.mate != null && data.mate !== null) {
evalType = 'mate';
evalValue = data.mate;
} else {
evalValue = parseFloat(evalValue);
}
if (State.playerColor === 'b' && evalType === 'cp') {
evalValue = -evalValue;
}
resolve({
move: moveMatch[1],
eval: { type: evalType, value: evalValue },
depth: depth,
ponder: null,
continuation: data.continuation ? data.continuation.split(' ') : [],
source: 'stockfish_online',
});
} else {
reject('API returned error');
}
} catch (e) {
reject(e);
}
},
onerror: (e) => reject(e),
ontimeout: () => reject('timeout'),
});
});
},
// Unified API call with fallback chain
analyze: async (fen, depth) => {
const tryApi = async (method, name) => {
try {
const result = await method(fen, depth);
State.apiAvailable = true;
State.apiFailCount = 0;
return result;
} catch (e) {
Utils.log(`${name} failed: ${e}`, 'warn');
return null;
}
};
// Try based on active engine type (rotation-aware)
const activeType = Account.currentEngine().type;
if (activeType === 'api' || activeType === 'stockfish_online') {
// Try primary
let result = null;
if (activeType === 'api') {
result = await tryApi(APIEngine.analyzeChessApi, 'chess-api.com');
if (!result) result = await tryApi(APIEngine.analyzeStockfishOnline, 'stockfish.online');
} else {
result = await tryApi(APIEngine.analyzeStockfishOnline, 'stockfish.online');
if (!result) result = await tryApi(APIEngine.analyzeChessApi, 'chess-api.com');
}
if (result) return result;
// Both APIs failed
State.apiFailCount++;
if (State.apiFailCount >= 3) {
State.apiAvailable = false;
Utils.log('APIs unreachable, falling back to local engine', 'error');
}
}
return null; // will trigger local fallback
}
};
// ═══════════════════════════════════════════
// LOCAL ENGINE (Stockfish.js 10.0.2 fallback)
// ═══════════════════════════════════════════
const LocalEngine = {
init: async () => {
if (State.workers.stockfish) return;
Utils.log('Initializing local Stockfish fallback...');
try {
const scriptContent = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://unpkg.com/[email protected]/stockfish.js',
timeout: 15000,
onload: (res) => resolve(res.responseText),
onerror: (err) => reject(err),
ontimeout: () => reject('timeout'),
});
});
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.engineReady = true;
Utils.log('Local Stockfish ready (fallback)');
}
if (msg.startsWith('bestmove')) {
const move = msg.split(' ')[1];
if (State._localResolve) {
State._localResolve(move);
State._localResolve = null;
}
}
if (msg.startsWith('info') && msg.includes('score')) {
LocalEngine.parseScore(msg);
}
};
State.workers.stockfish.postMessage('uci');
State.workers.stockfish.postMessage('isready');
State.workers.stockfish.postMessage(`setoption name MultiPV value ${CONFIG.multiPV}`);
} catch (e) {
Utils.log('Local Stockfish init failed: ' + e, 'error');
}
},
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;
State.candidates[mpv] = {
move: moveMatch[1],
eval: { type, value },
depth,
};
if (mpv === 1) {
State.currentEval = { type, value, depth };
State.currentBestMove = moveMatch[1];
}
if (mpv === 1 && msg.includes(' pv ')) {
const pvMoves = msg.split(' pv ')[1].split(' ');
if (pvMoves.length > 1) {
State.opponentResponse = pvMoves[1];
}
}
}
},
// Analyze with local SF.js - returns promise that resolves with best move
analyze: (fen, depth) => {
if (!State.workers.stockfish || !State.engineReady) return Promise.resolve(null);
return new Promise((resolve) => {
// For local SF 10.0.2, cap depth to prevent hanging
// SF10 can't handle high depths efficiently
const safedepth = Math.min(depth, 12);
State.candidates = {};
State._localResolve = resolve;
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 ${safedepth}`);
// Safety timeout: if SF doesn't respond in 10s, resolve null
setTimeout(() => {
if (State._localResolve === resolve) {
State._localResolve = null;
Utils.log('Local SF timeout, resolving with current best', 'warn');
resolve(State.currentBestMove);
}
}, 10000);
});
},
// Quick analysis for multi-PV candidates (runs alongside API)
analyzeForCandidates: (fen) => {
if (!State.workers.stockfish || !State.engineReady) return;
State.candidates = {};
State.workers.stockfish.postMessage('stop');
State.workers.stockfish.postMessage(`position fen ${fen}`);
State.workers.stockfish.postMessage(`setoption name MultiPV value ${CONFIG.multiPV}`);
// Use low depth for quick candidates - just need alternatives, not perfect eval
State.workers.stockfish.postMessage(`go depth ${Math.min(10, CONFIG.engineDepth.base)}`);
},
};
// ═══════════════════════════════════════════
// UNIFIED ENGINE
// ═══════════════════════════════════════════
const Engine = {
init: async () => {
UI.updateStatus('orange');
// Always init local engine as fallback
await LocalEngine.init();
// Test API availability
if (CONFIG.engineType !== 'local') {
Utils.log('Testing API engine...');
try {
const testResult = await APIEngine.analyze('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', 8);
if (testResult) {
Utils.log(`API engine OK: ${testResult.source} returned ${testResult.move}`, 'info');
State.apiAvailable = true;
} else {
Utils.log('API engines unavailable, will use local fallback', 'warn');
State.apiAvailable = false;
}
} catch (e) {
State.apiAvailable = false;
}
}
UI.updateStatus(UI._styleAccents[CONFIG.playStyle] || '#4caf50');
HumanStrategy.initGamePersonality();
},
analyze: async (fen) => {
if (State.isThinking) return;
State.isThinking = true;
State.candidates = {};
State.apiResult = null;
State._lastEngineSource = null;
// --- Opening book check (with tapering — item 7) ---
// Real players don't have a hard cutoff where they leave book.
// They gradually forget their prep — first few moves are automatic,
// then the probability of still being "in book" drops off.
if (CONFIG.useBook && State.moveCount < 20) {
let bookChance = 1.0;
if (State.moveCount > 14) bookChance = 0.25;
else if (State.moveCount > 10) bookChance = 0.50;
else if (State.moveCount > 7) bookChance = 0.80;
if (Math.random() < bookChance) {
const bookMove = await OpeningBook.fetchMove(fen);
if (bookMove) {
Utils.log(`Book Move: ${bookMove} (bookChance=${(bookChance*100).toFixed(0)}%)`);
UI.toast('Opening Book', `Playing known book move ${bookMove}`, 'book', 3000);
State._lastEngineSource = 'book';
Engine.handleResult(bookMove, fen, true);
return;
}
}
}
// --- Tablebase check ---
if (CONFIG.useTablebase) {
const tbResult = await Tablebase.probe(fen);
if (tbResult) {
Utils.log(`Tablebase Move: ${tbResult.move} (${tbResult.category})`);
UI.toast('Tablebase', `Perfect endgame: ${tbResult.move} (${tbResult.category})`, 'tablebase', 3500);
State._lastEngineSource = 'tablebase';
// Set eval based on tablebase result
State.currentEval = {
type: tbResult.category === 'win' ? 'mate' : 'cp',
value: tbResult.category === 'win' ? Math.abs(tbResult.dtz) : 0,
};
Engine.handleResult(tbResult.move, fen, false);
return;
}
}
// --- Fetch player DB moves in parallel (non-blocking) ---
const playerDBPromise = PlayerMoveDB.fetch(fen);
// --- Main engine analysis (rotation-aware) ---
const engineInfo = Account.currentEngine();
const activeType = engineInfo.type;
let depth = HumanStrategy.getDynamicDepth(fen);
if (engineInfo.depthCap != null) {
depth = Math.min(depth, engineInfo.depthCap);
}
Utils.log(`Analyzing depth ${depth} (engine: ${activeType}${engineInfo.depthCap ? ', shallow-rotation' : ''})`, 'debug');
// Start local SF for multi-PV candidates in parallel
if (activeType !== 'local') {
LocalEngine.analyzeForCandidates(fen);
}
// Try API engine first (if configured)
if (activeType !== 'local' && State.apiAvailable) {
const apiResult = await APIEngine.analyze(fen, depth);
if (apiResult) {
State.apiResult = apiResult;
State._lastEngineSource = apiResult.source;
// Use API result as PV1, overriding local SF's PV1
State.currentEval = apiResult.eval;
State.currentBestMove = apiResult.move;
State.candidates[1] = {
move: apiResult.move,
eval: apiResult.eval,
depth: apiResult.depth,
};
// Extract opponent response from continuation
if (apiResult.continuation && apiResult.continuation.length > 1) {
State.opponentResponse = apiResult.continuation[1];
}
// Wait a tiny bit for local SF to populate PV2-5 candidates
await Utils.sleep(500);
Engine.handleResult(apiResult.move, fen, false);
return;
}
}
// Fallback: use local SF.js for everything
State._lastEngineSource = 'local';
const localBest = await LocalEngine.analyze(fen, depth);
if (localBest) {
Engine.handleResult(localBest, fen, false);
} else {
State.isThinking = false;
Utils.log('All engines failed!', 'error');
UI.updateStatus('red');
}
},
handleResult: async (bestMove, fen, isBook) => {
State.isThinking = false;
State.moveCount++;
// Update time pressure multipliers before move selection
HumanStrategy.updateTimePressure();
// --- Opponent-move surprise detection ---
// Compare eval now vs eval before opponent moved — big swing = surprising move
if (State.human.evalBeforeOpponentMove != null && State.currentEval && State.currentEval.type === 'cp') {
const swing = Math.abs(State.currentEval.value - State.human.evalBeforeOpponentMove);
State.human.opponentMoveSurprise = Math.min(1.0, Math.max(0, (swing - 0.3) / 1.7));
} else {
State.human.opponentMoveSurprise = 0;
}
const phase = HumanStrategy.getGamePhase(fen);
// Check auto-accept draw when losing
if (CONFIG.auto.enabled && !isBook && State.currentEval) {
const drawOffer = document.querySelector(SELECTORS.drawOffer);
if (drawOffer && State.currentEval.value <= -1.0) {
const acceptBtn = drawOffer.querySelector('button, [class*="accept"], [data-cy*="accept"]') || drawOffer;
if (acceptBtn) {
const drawDelay = Utils.humanDelay(1500, 5000);
Utils.log(`Auto-Accept Draw: eval ${State.currentEval.value}, accepting in ${Math.round(drawDelay)}ms`, 'warn');
UI.toast('Draw Accepted', `Eval ${State.currentEval.value.toFixed(1)} — accepting draw offer`, 'draw', 5000);
await Utils.sleep(drawDelay);
acceptBtn.click();
return;
}
}
}
// --- Draw offer behavior (item 8) ---
// Real humans offer draws in dead-equal, simplified positions after move 35+
// Never offering draws across hundreds of games is a detectable pattern
if (CONFIG.auto.enabled && !isBook && State.currentEval && State.moveCount >= 35) {
const ev = State.currentEval.value;
const pieces = Utils.countPieces(fen);
const isDrawish = Math.abs(ev) < 0.25 && pieces <= 14; // equal + simplified
const isLongGame = State.moveCount >= 50;
if (isDrawish || (isLongGame && Math.abs(ev) < 0.5)) {
// 8% chance per qualifying move to offer a draw
if (Math.random() < 0.08) {
const drawBtn = document.querySelector('[data-cy="draw"], [aria-label*="draw" i], .draw-button-component button, button[class*="draw"]');
if (drawBtn) {
const drawDelay = Utils.humanDelay(2000, 6000);
Utils.log(`Draw Offer: eval ${ev.toFixed(2)}, ${pieces} pieces, move ${State.moveCount}`, 'info');
UI.toast('Draw Offered', `Position is dead equal — offering draw`, 'draw', 4000);
await Utils.sleep(drawDelay);
drawBtn.click();
// Don't return — still play a move in case they decline
}
}
}
}
// Check auto-resign before playing (don't waste a move if we're resigning)
if (CONFIG.auto.enabled && !isBook) {
const resigned = await HumanStrategy.checkAutoResign();
if (resigned) return;
}
// Use HumanStrategy to decide which move to actually play
const moveResult = HumanStrategy.selectMove(fen, bestMove, isBook);
const finalMove = moveResult.move;
const modeTag = State.human.autoLoseActive ? ' [AUTO-LOSE]' : '';
const tpTag = State.human.timePressureMult.suboptimal > 1 ? ` [TP x${State.human.timePressureMult.suboptimal}]` : '';
Utils.log(`Move #${State.moveCount} [${phase}]: ${finalMove} (${moveResult.reason})${moveResult.isBest ? '' : ' [SUBOPTIMAL]'}${modeTag}${tpTag}`);
// Toast notification based on move type
const evalStr = State.currentEval ? (State.currentEval.type === 'mate' ? `M${State.currentEval.value}` : `${State.currentEval.value > 0 ? '+' : ''}${State.currentEval.value.toFixed(1)}`) : '?';
if (moveResult.reason === 'blunder') {
UI.toast('Blunder', `Intentional mistake: ${finalMove} (eval ${evalStr})`, 'blunder', 4500);
} else if (moveResult.reason === 'suboptimal') {
UI.toast('Suboptimal', `Playing 2nd/3rd choice: ${finalMove} instead of ${bestMove}`, 'suboptimal', 3500);
} else if (moveResult.reason === 'random-legal') {
UI.toast('Random Move', `Anti-correlation: ${finalMove} (not from engine PV)`, 'fakeout', 4000);
} else if (moveResult.reason === 'missed-tactic') {
UI.toast('Missed Tactic', `Played safe instead of small tactic: ${finalMove}`, 'suboptimal', 4000);
} else if (moveResult.reason === 'close-alt') {
UI.toast('Close Alternative', `${finalMove} nearly equal to ${bestMove} — diversifying`, 'move', 3000);
} else if (moveResult.reason === 'correlation-cap') {
UI.toast('Anti-Correlation', `Top move rate too high — playing ${finalMove}`, 'fakeout', 3500);
} else if (moveResult.reason === 'player-db') {
UI.toast('Human Move', `Real players at this rating play ${finalMove}`, 'book', 3500);
} else if (moveResult.reason === 'book') {
// already toasted above
} else {
UI.toast(`Move #${State.moveCount}`, `Best: ${finalMove} [${phase}] (${evalStr})`, 'move', 2500);
}
// Draw arrows
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 });
// Track for correlation management
HumanStrategy.trackMove(moveResult.isBest, moveResult);
// Store predicted opponent reply for premove simulation
if (State.opponentResponse) {
State.human.predictedReply = State.opponentResponse;
State.human.predictedReplyFen = fen;
}
// Auto-play
if (CONFIG.auto.enabled && Game.isMyTurn(fen)) {
// ANTI-DETECTION: Random AFK delay (simulates human distraction)
const afk = CONFIG.antiDetection.randomAFK;
if (afk.enabled && Math.random() < afk.chance) {
const afkDelay = Utils.humanDelay(afk.delay.min, afk.delay.max);
Utils.log(`AFK pause: ${Math.round(afkDelay)}ms (simulating distraction)`, 'debug');
UI.toast('AFK Pause', `Simulating distraction for ${(afkDelay / 1000).toFixed(1)}s`, 'afk', Math.min(afkDelay, 5000));
await Utils.sleep(afkDelay);
// Re-check position hasn't changed during AFK
if (Game.getFen() !== fen) {
Utils.log('Position changed during AFK, aborting', 'warn');
return;
}
}
const delay = HumanStrategy.calculateDelay(fen, moveResult, isBook);
State.human.lastThinkTime = delay; // store for momentum tracking
Utils.log(`Waiting ${Math.round(delay)}ms (${moveResult.reason})...`);
await Utils.sleep(delay);
// Verify position hasn't changed during delay
const currentFen = Game.getFen();
if (currentFen === fen) {
await Humanizer.executeMove(finalMove);
} else {
Utils.log('Position changed during delay, aborting move', 'warn');
}
}
}
};
// ═══════════════════════════════════════════
// HUMAN STRATEGY
// ═══════════════════════════════════════════
const HumanStrategy = {
getGamePhase: (fen) => {
if (!fen) return 'middlegame';
const board = fen.split(' ')[0];
const moveNum = State.moveCount;
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';
},
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;
let complexity = (pieces + pawns * 0.5) / 24;
if (State.currentEval && Math.abs(State.currentEval.value) < 0.5) {
complexity += 0.2;
}
// More pawns in center = more tactical complexity
const ranks = board.split('/');
let centerPawns = 0;
for (const rank of ranks) {
let col = 0;
for (const ch of rank) {
if (ch >= '1' && ch <= '8') col += parseInt(ch);
else { if ((col === 3 || col === 4) && (ch === 'p' || ch === 'P')) centerPawns++; col++; }
}
}
if (centerPawns >= 2) complexity += 0.15;
if (pieces > 10) complexity += 0.1;
return Math.min(1, Math.max(0, complexity));
},
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(`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');
},
resetGame: () => {
State.human.perfectStreak = 0;
State.human.sloppyStreak = 0;
State.human.bestMoveCount = 0;
State.human.totalMoveCount = 0;
State.human.lastMoveWasBest = true;
State.human.clusterMode = 'normal';
State.human.clusterMovesLeft = 0;
State.human.predictedReply = null;
State.human.consecutiveLosingEvals = 0;
State.human.autoLoseActive = false;
// Reset messy-resign decision cache each game
State.human.resignDecisionMade = false;
State.human.resignExtraMoves = null;
State.human.resignBlunderNext = false;
State.human.resignHolding = false;
State.human.timePressureMult = { suboptimal: 1, blunder: 1, maxCPLoss: 1 };
State.human.topMoveCount = 0;
State.human._prevEval = 0;
State.human.thinkCategory = 'normal';
State.human.opponentMoveSurprise = 0;
State.human.lastThinkTime = 0;
State.human.evalBeforeOpponentMove = null;
// NOTE: playerTempo and playerAccuracyBand are NOT reset — they persist per account
State.lastMoveWasCapture = false;
State.moveCount = 0;
State.candidates = {};
State.recentTimings = [];
HumanStrategy.initGamePersonality();
// ---------- Account-level setup for this game ----------
Account.ensureHardwarePersona();
Account.ensureRepertoire();
Account.rollEngineForGame();
// Apply tilt for this game if active
const tiltCfg = Account.consumeTiltTick();
State.human.tiltActive = !!tiltCfg;
if (tiltCfg) {
Utils.log(`Tilt active this game: subopt+${tiltCfg.suboptimalBoost}, blunder x${tiltCfg.blunderMult}, timing x${tiltCfg.timingMult}`, 'warn');
}
// Determine effective target rating for this game.
// Priority: warmup ramp > opponent adaptation > base.
let effectiveTarget = Account.effectiveTargetRating();
const inWarmup = Account.isInWarmup();
if (inWarmup) {
Utils.log(`Warmup: game ${CONFIG.account.totalGamesPlayed + 1}/${CONFIG.warmup.durationGames}, target=${effectiveTarget} (base ${CONFIG.targetRating})`, 'info');
UI.toast('Warmup', `Game ${CONFIG.account.totalGamesPlayed + 1}/${CONFIG.warmup.durationGames} - playing as ~${effectiveTarget}`, 'info', 3500);
}
// Opponent adaptation: read opponent rating and adjust target.
// Skip when in warmup (warmup target wins).
if (CONFIG.opponentAdaptation.enabled && !inWarmup) {
const oppRating = Game.readOpponentRating();
if (oppRating) {
State.human.opponentRating = oppRating;
effectiveTarget = Math.max(
CONFIG.opponentAdaptation.minRating,
Math.min(CONFIG.opponentAdaptation.maxRating, oppRating + CONFIG.opponentAdaptation.ratingEdge)
);
Utils.log(`Opponent Adaptation: Opponent is ${oppRating}, targeting ${effectiveTarget} (edge: +${CONFIG.opponentAdaptation.ratingEdge})`, 'info');
} else {
Utils.log('Opponent Adaptation: Could not read opponent rating, using default', 'debug');
}
}
RatingProfile.apply(effectiveTarget);
// Auto-lose mode — adaptive instead of binary trigger.
//
// Old behavior: when streak >= triggerStreak, ALWAYS throw the game.
// That produces an obvious pattern: "wins N in a row, then a clean
// textbook resign". Real players don't do that; they slowly degrade.
//
// New behavior: from (triggerStreak - 2) onwards, the probability of
// throwing this game ramps smoothly with each additional win, so
// some streaks naturally end in losses earlier and others go a game
// or two longer.
if (CONFIG.autoLose.enabled) {
const streak = CONFIG.session.currentWinStreak;
const trigger = CONFIG.autoLose.triggerStreak;
let loseChance = 0;
if (streak >= trigger) loseChance = 0.85;
else if (streak === trigger - 1) loseChance = 0.45;
else if (streak === trigger - 2) loseChance = 0.18;
// Session winrate bump (short-window, intra-session)
const wr = CONFIG.session.gamesPlayed > 4
? CONFIG.session.wins / CONFIG.session.gamesPlayed
: 0;
if (wr >= 0.85) loseChance = Math.max(loseChance, 0.35);
// Lifetime winrate-overshoot bump (sliding window across sessions)
const overshootBoost = Account.winrateOvershootBoost();
if (overshootBoost > 0) {
loseChance = Math.min(0.95, loseChance + overshootBoost);
const wrLong = Account.recentWinrate();
Utils.log(`WinrateTarget: long-window WR ${(wrLong*100).toFixed(0)}% > target ${(CONFIG.winrateTarget.target*100).toFixed(0)}% -> +${(overshootBoost*100).toFixed(0)}% lose chance`, 'warn');
}
if (loseChance > 0 && Math.random() < loseChance) {
State.human.autoLoseActive = true;
Utils.log(`AUTO-LOSE: streak=${streak} sessWR=${wr.toFixed(2)} boost=${overshootBoost.toFixed(2)} -> p(lose)=${loseChance.toFixed(2)} ROLLED`, 'error');
UI.toast('Throwing this one', `Streak ${streak} - playing to lose for cover`, 'error', 6000);
} else if (loseChance > 0) {
Utils.log(`Auto-Lose: p(lose)=${loseChance.toFixed(2)} did NOT roll, playing normally`, 'warn');
}
}
},
// Update time pressure accuracy multipliers based on current clock
updateTimePressure: () => {
const tp = CONFIG.timePressure;
if (!tp.enabled || State.clock.myTime == null) {
State.human.timePressureMult = { suboptimal: 1, blunder: 1, maxCPLoss: 1 };
return;
}
for (const t of tp.thresholds) {
if (State.clock.myTime <= t.secondsBelow) {
State.human.timePressureMult = {
suboptimal: t.suboptimalMult,
blunder: t.blunderMult,
maxCPLoss: t.maxCPLossMult,
};
Utils.log(`Time Pressure: ${State.clock.myTime}s left, subopt x${t.suboptimalMult}, blunder x${t.blunderMult}`, 'debug');
if (t.suboptimalMult >= 2) UI.toast('Time Pressure', `${State.clock.myTime}s left — accuracy dropping`, 'time', 3000);
return;
}
}
State.human.timePressureMult = { suboptimal: 1, blunder: 1, maxCPLoss: 1 };
},
// Check if auto-resign should trigger
checkAutoResign: async () => {
const ar = CONFIG.autoResign;
if (!ar.enabled) return false;
if (State.moveCount < ar.minMoveNumber) return false;
const eval_ = State.currentEval;
if (!eval_) return false;
if (eval_.value <= ar.evalThreshold || (eval_.type === 'mate' && eval_.value < 0)) {
State.human.consecutiveLosingEvals++;
Utils.log(`Auto-Resign: Losing eval #${State.human.consecutiveLosingEvals}/${ar.consecutiveMoves} (eval: ${eval_.value})`, 'debug');
} else {
State.human.consecutiveLosingEvals = 0;
// Eval recovered — clear any messy-resign scheduling
State.human.resignExtraMoves = null;
State.human.resignBlunderNext = false;
State.human.resignHolding = false;
}
if (State.human.consecutiveLosingEvals < ar.consecutiveMoves) return false;
const mr = CONFIG.messyResign;
// First time we hit the threshold for this lost position — roll all
// the behavioral dice ONCE and cache the outcome so we're consistent
// for the rest of the game.
if (mr.enabled && State.human.resignDecisionMade !== true) {
State.human.resignDecisionMade = true;
// Roll: hold the lost position (refuse to resign entirely)?
if (Math.random() < mr.holdLostChance) {
State.human.resignHolding = true;
Utils.log(`MessyResign: Chose to HOLD lost position (no resign this game)`, 'warn');
UI.toast('Stubborn', `Not resigning — playing it out`, 'warn', 4000);
return false;
}
// Roll: play some extra moves before resigning (looks like contemplation)
const extra = mr.extraMovesBeforeResign;
State.human.resignExtraMoves = Math.round(Utils.randomRange(extra.min, extra.max));
// Roll: blunder once more before resigning
State.human.resignBlunderNext = Math.random() < mr.blunderBeforeChance;
Utils.log(`MessyResign: will play ${State.human.resignExtraMoves} more move(s)${State.human.resignBlunderNext ? ' with a blunder' : ''} before resigning`, 'warn');
}
if (State.human.resignHolding) return false;
// If we have remaining "extra moves before resign", count this check as
// one, and signal to the caller that we're NOT resigning this turn.
// The actual blunder behavior (if scheduled) is wired into selectMove.
if (mr.enabled && State.human.resignExtraMoves > 0) {
State.human.resignExtraMoves--;
Utils.log(`MessyResign: playing on, ${State.human.resignExtraMoves} more moves before resign`, 'debug');
return false;
}
// Fallback / disabled mode: use the old random-chance gate
if (!mr.enabled && Math.random() >= ar.resignChance) {
Utils.log('Auto-Resign: Skipped (random chance - playing on like a stubborn human)', 'debug');
State.human.consecutiveLosingEvals = 0;
return false;
}
const delay = Utils.humanDelay(ar.delay.min, ar.delay.max);
Utils.log(`Auto-Resign: Triggering in ${Math.round(delay)}ms (eval: ${eval_.value}, ${State.human.consecutiveLosingEvals} consecutive)`, 'warn');
UI.toast('Auto-Resign', `Eval ${eval_.value.toFixed(1)} for ${State.human.consecutiveLosingEvals} moves - resigning`, 'resign', 5000);
await Utils.sleep(delay);
return await Game.resign();
},
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;
if (complexity < 0.3) depth -= 2;
else if (complexity > 0.7) depth += 2;
if (phase === 'endgame') depth += 1;
depth += Math.round(Math.random() * 2 - 1);
return Math.max(cfg.min, Math.min(cfg.max, depth));
},
// Accuracy clustering: simulate hot/cold streaks
updateCluster: () => {
const ac = CONFIG.humanization.accuracyClustering;
if (!ac?.enabled) return;
if (State.human.clusterMovesLeft > 0) {
State.human.clusterMovesLeft--;
if (State.human.clusterMovesLeft === 0) {
Utils.log(`Cluster: Exiting ${State.human.clusterMode} mode`, 'debug');
State.human.clusterMode = 'normal';
}
return;
}
// Chance to enter a streak
const r = Math.random();
if (r < ac.hotStreakChance) {
State.human.clusterMode = 'hot';
State.human.clusterMovesLeft = Math.round(Utils.randomRange(ac.streakDuration.min, ac.streakDuration.max));
Utils.log(`Cluster: Entering HOT streak (${State.human.clusterMovesLeft} moves)`, 'debug');
UI.toast('Hot Streak', `Playing sharp for ${State.human.clusterMovesLeft} moves`, 'streak', 3000);
} else if (r < ac.hotStreakChance + ac.coldStreakChance) {
State.human.clusterMode = 'cold';
State.human.clusterMovesLeft = Math.round(Utils.randomRange(ac.streakDuration.min, ac.streakDuration.max));
Utils.log(`Cluster: Entering COLD streak (${State.human.clusterMovesLeft} moves)`, 'debug');
UI.toast('Cold Streak', `Playing sloppy for ${State.human.clusterMovesLeft} moves`, 'warn', 3000);
}
},
shouldPlaySuboptimal: (fen, extraBoost = 0) => {
const h = CONFIG.humanization;
if (!h.enabled) return false;
// AUTO-LOSE MODE: massively increase error rate
if (State.human.autoLoseActive && State.moveCount >= CONFIG.autoLose.minMovesBeforeLosing) {
const rate = CONFIG.autoLose.suboptimalRate;
Utils.log(`Auto-Lose: suboptimal rate ${(rate * 100).toFixed(0)}%`, 'debug');
return Math.random() < rate;
}
const phase = HumanStrategy.getGamePhase(fen);
const personality = State.human.gamePersonality || { suboptimalMult: 1 };
const eval_ = State.currentEval;
let rate = h.suboptimalMoveRate[phase] || 0.25;
rate *= personality.suboptimalMult;
// Persistent per-account accuracy band (item 5: multi-game consistency)
rate += State.human.playerAccuracyBand;
// Apply extra boost from timing-accuracy coupling and weakness profile
rate += extraBoost;
// Time pressure accuracy drop
rate *= State.human.timePressureMult.suboptimal;
// Tilt: active after a loss — additive suboptimal boost
if (State.human.tiltActive && CONFIG.tilt?.enabled) {
rate += CONFIG.tilt.suboptimalBoost;
}
// Accuracy cluster modifier
if (State.human.clusterMode === 'hot') {
rate *= 0.3; // 70% fewer errors during hot streak
} else if (State.human.clusterMode === 'cold') {
rate *= 1.3; // 30% more errors during cold streak
}
// Winning degradation
if (h.winningDegradation.enabled && eval_) {
const adv = eval_.value;
for (const tier of h.winningDegradation.tiers) {
if (adv >= tier.evalAbove) {
rate += tier.extraSuboptimalRate;
}
}
}
// Losing sharpness
if (h.losingSharpness.enabled && eval_ && eval_.value <= h.losingSharpness.evalBelow) {
rate *= h.losingSharpness.suboptimalReduction;
}
// Streak management
if (h.streaks.enabled) {
if (State.human.perfectStreak >= h.streaks.perfectStreakMax) {
Utils.log('Streak: Forcing suboptimal after perfect run', 'debug');
return true;
}
if (State.human.sloppyStreak >= h.streaks.sloppyStreakMax) {
Utils.log('Streak: Forcing best after sloppy run', 'debug');
return false;
}
}
// Engine correlation guard
if (State.human.totalMoveCount >= 8) {
const currentCorrelation = State.human.bestMoveCount / State.human.totalMoveCount;
if (currentCorrelation > h.targetEngineCorrelation + 0.05) {
rate += 0.08;
Utils.log(`Correlation guard: ${(currentCorrelation * 100).toFixed(0)}% > target, boosting suboptimal`, 'debug');
} else if (currentCorrelation < h.targetEngineCorrelation - 0.10) {
rate *= 0.3;
Utils.log(`Correlation guard: ${(currentCorrelation * 100).toFixed(0)}% < target, reducing suboptimal`, 'debug');
}
}
// Cap: never go above 45% even with all multipliers stacked
// When losing, cap even lower — don't throw away a game that's close
const maxRate = (eval_ && eval_.value < -0.5) ? 0.10 : 0.35;
rate = Math.max(0.03, Math.min(maxRate, rate));
return Math.random() < rate;
},
pickSuboptimalMove: (fen) => {
const phase = HumanStrategy.getGamePhase(fen);
let maxCPLoss = State.human.autoLoseActive
? CONFIG.autoLose.maxCPLoss
: (CONFIG.humanization.maxAcceptableCPLoss[phase] || 60);
// Time pressure: allow slightly bigger mistakes, but cap the multiplier
maxCPLoss *= Math.min(1.5, State.human.timePressureMult.maxCPLoss);
// HARD SAFETY CAP: never allow suboptimal moves that lose a piece (300cp = minor piece+pawn)
// unless in auto-lose mode
if (!State.human.autoLoseActive) {
maxCPLoss = Math.min(maxCPLoss, 200);
}
const candidates = State.candidates;
const bestEval = candidates[1]?.eval;
if (!bestEval || Object.keys(candidates).length < 2) {
return candidates[1]?.move || null;
}
const alternatives = [];
for (let i = 2; i <= CONFIG.multiPV; i++) {
const c = candidates[i];
if (!c || !c.eval || !c.move) continue;
// Self-awareness: don't make suboptimal Queen/King moves early in the game
const fromSq = c.move.substring(0, 2);
const pieceVal = HumanStrategy.getPieceValueFromFen(fen, fromSq);
if (pieceVal >= 800 && State.moveCount < 30) continue;
let cpLoss;
if (bestEval.type === 'mate' && c.eval.type === 'mate') {
cpLoss = Math.abs(c.eval.value - bestEval.value) * 50;
} else if (bestEval.type === 'mate') {
cpLoss = 300; // losing a forced mate is always bad
} else if (c.eval.type === 'mate' && c.eval.value > 0) {
cpLoss = 0;
} else if (c.eval.type === 'mate' && c.eval.value < 0) {
cpLoss = 900; // getting mated = worst possible
} 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;
}
// Weight: use a log-normal-like distribution to match real human CP loss curves
// Real humans have: lots of 10-35cp mistakes, fewer 40-80cp, rare 100-200cp
// Pure exponential decay (old) creates too many 0-5cp "errors" that look engine-like
const weights = alternatives.map(a => {
const cp = Math.max(a.cpLoss, 1); // avoid log(0)
// Log-normal peak around 20-35cp, gentle tail toward larger losses
const logCp = Math.log(cp);
const mu = 3.2; // ln(~25cp) = peak of the distribution
const sigma = 0.9;
return Math.exp(-Math.pow(logCp - mu, 2) / (2 * sigma * sigma)) / cp;
});
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: PV${alternatives[i].pvIndex} (${alternatives[i].move}, -${alternatives[i].cpLoss.toFixed(0)}cp)`, 'warn');
return alternatives[i].move;
}
}
return alternatives[0].move;
},
shouldBlunder: (fen) => {
const b = CONFIG.humanization.blunder;
if (!b || b.chance <= 0) return false;
const eval_ = State.currentEval;
if (!eval_) return false;
// AUTO-LOSE MODE: blunder frequently regardless of position
if (State.human.autoLoseActive && State.moveCount >= CONFIG.autoLose.minMovesBeforeLosing) {
const chance = CONFIG.autoLose.blunderChance;
Utils.log(`Auto-Lose: blunder chance ${(chance * 100).toFixed(0)}%`, 'debug');
return Math.random() < chance;
}
if (eval_.value >= b.disableWhenEvalBetween[0] && eval_.value <= b.disableWhenEvalBetween[1]) return false;
if (b.onlyInComplexPositions && HumanStrategy.getPositionComplexity(fen) < 0.4) return false;
if (eval_.value < 0) return false;
// Cluster: never blunder during hot streak, double chance during cold
let chance = b.chance;
if (State.human.clusterMode === 'hot') return false;
if (State.human.clusterMode === 'cold') chance *= 2;
// Time pressure: much more likely to blunder under time trouble
chance *= State.human.timePressureMult.blunder;
// Tilt: multiply blunder chance for N games after a loss
if (State.human.tiltActive && CONFIG.tilt?.enabled) {
chance *= CONFIG.tilt.blunderMult;
}
return Math.random() < chance;
},
pickBlunderMove: () => {
const fen = State.lastFen;
const candidates = State.candidates;
const bestEval = candidates[1]?.eval;
if (!bestEval) return null;
let maxLoss = State.human.autoLoseActive
? CONFIG.autoLose.maxCPLoss
: CONFIG.humanization.blunder.maxCPLoss;
// Cap time pressure blunder loss to 1.3x — don't hang queens just because clock is low
maxLoss *= Math.min(1.3, State.human.timePressureMult.maxCPLoss);
// HARD CAP: blunders should lose a pawn or minor piece, not the queen
if (!State.human.autoLoseActive) maxLoss = Math.min(maxLoss, 300);
const blunderCandidates = [];
for (let i = 2; i <= CONFIG.multiPV; i++) {
const c = candidates[i];
if (!c || !c.eval || !c.move) continue;
// Self-awareness: don't blunder the Queen/King early in the game
if (fen) {
const fromSq = c.move.substring(0, 2);
const pieceVal = HumanStrategy.getPieceValueFromFen(fen, fromSq);
if (pieceVal >= 800 && State.moveCount < 30) continue;
}
let cpLoss;
if (c.eval.type === 'mate' && c.eval.value < 0) {
cpLoss = 900; // getting mated
} else if (bestEval.type === 'cp' && c.eval.type === 'cp') {
cpLoss = (bestEval.value - c.eval.value) * 100;
} else {
continue;
}
// Only consider moves that lose between 30cp and maxLoss
// (below 30cp is an inaccuracy, not a blunder)
if (cpLoss >= 30 && cpLoss <= maxLoss) {
blunderCandidates.push({ move: c.move, cpLoss, pvIndex: i });
}
}
if (blunderCandidates.length === 0) return null;
// Pick randomly (weighted toward mid-range losses, not the absolute worst)
const weights = blunderCandidates.map(a => Math.exp(-Math.abs(a.cpLoss - maxLoss * 0.4) / 50));
const totalWeight = weights.reduce((s, w) => s + w, 0);
let r = Math.random() * totalWeight;
for (let i = 0; i < blunderCandidates.length; i++) {
r -= weights[i];
if (r <= 0) {
Utils.log(`BLUNDER: ${blunderCandidates[i].move} (-${blunderCandidates[i].cpLoss.toFixed(0)}cp)`, 'error');
return blunderCandidates[i].move;
}
}
const pick = blunderCandidates[0];
Utils.log(`BLUNDER: ${pick.move} (-${pick.cpLoss.toFixed(0)}cp)`, 'error');
return pick.move;
},
// Pick a random legal move NOT in the engine PV list (breaks correlation fingerprint)
pickRandomLegalMove: (fen) => {
try {
const g = Game.getBoardGame();
if (!g || !g.getLegalMoves) return null;
const legalMoves = g.getLegalMoves();
if (!legalMoves || legalMoves.length === 0) return null;
const uciMoves = [];
for (const m of legalMoves) {
if (m.from && m.to) {
uciMoves.push(m.from + m.to + (m.promotion || ''));
} else if (typeof m === 'string') {
uciMoves.push(m);
}
}
const pvMoves = new Set();
for (let i = 1; i <= CONFIG.multiPV; i++) {
if (State.candidates[i]?.move) pvMoves.add(State.candidates[i].move.substring(0, 4));
}
let nonPvMoves = uciMoves.filter(m => !pvMoves.has(m.substring(0, 4)));
if (nonPvMoves.length === 0) return null;
// Filter by max CP loss if we have eval data from PV candidates
const bestEval = State.candidates[1]?.eval;
if (bestEval && bestEval.type === 'cp') {
const maxLoss = CONFIG.antiDetection.randomLegalMaxCPLoss || 250;
// We can't know exact eval of random moves, but avoid obviously bad ones:
// exclude moves that hang pieces (move to a square attacked and not defended)
// Simple heuristic: avoid moving king, prefer pawn pushes for "random" moves
nonPvMoves = nonPvMoves.filter(m => {
// Self-awareness: NEVER move the Queen or King as a completely random legal move!
const fromSq = m.substring(0, 2);
const pieceVal = HumanStrategy.getPieceValueFromFen(fen, fromSq);
if (pieceVal >= 800) return false;
// Full board analysis: reject any move that hangs material
return HumanStrategy.isMoveSafe(m, fen);
});
if (nonPvMoves.length === 0) return null;
}
const pick = nonPvMoves[Math.floor(Math.random() * nonPvMoves.length)];
Utils.log(`Random legal move (non-PV): ${pick} — breaks engine correlation`, 'warn');
return pick;
} catch (e) {
return null;
}
},
// --- PIECE PROTECTION ---
// Piece values in centipawns
_pieceValues: { p: 100, n: 300, b: 320, r: 500, q: 900, k: 99999 },
// Parse FEN board into 8x8 array: board[rank][file] = char or null
// rank 0 = rank 1 (white's back rank), file 0 = a-file
_parseFenBoard: (fen) => {
const rows = fen.split(' ')[0].split('/').reverse(); // rank 1 first
const board = [];
for (let r = 0; r < 8; r++) {
board[r] = [];
let f = 0;
for (const ch of (rows[r] || '')) {
if (/\d/.test(ch)) { for (let i = 0; i < parseInt(ch); i++) board[r][f++] = null; }
else board[r][f++] = ch;
}
while (f < 8) board[r][f++] = null;
}
return board;
},
// Convert algebraic square to [rank, file] indices
_sqToIdx: (sq) => [parseInt(sq[1]) - 1, sq.charCodeAt(0) - 97],
// Get all squares that attack a given [rank, file], filtered by color
// color: 'w' or 'b' — which side's attackers to find
_getAttackers: (board, rank, file, color) => {
const attackers = [];
const isUpper = (ch) => ch && ch === ch.toUpperCase(); // white pieces
const isColor = (ch) => color === 'w' ? isUpper(ch) : (ch && !isUpper(ch));
const inBounds = (r, f) => r >= 0 && r < 8 && f >= 0 && f < 8;
const pv = HumanStrategy._pieceValues;
// Pawn attacks
const pawnDir = color === 'w' ? -1 : 1; // pawns of color attack FROM this direction
const pawnChar = color === 'w' ? 'P' : 'p';
for (const df of [-1, 1]) {
const pr = rank + pawnDir, pf = file + df;
if (inBounds(pr, pf) && board[pr][pf] === pawnChar) {
attackers.push({ r: pr, f: pf, piece: 'p', value: pv.p });
}
}
// Knight attacks
const knightChar = color === 'w' ? 'N' : 'n';
for (const [dr, df] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]) {
const nr = rank + dr, nf = file + df;
if (inBounds(nr, nf) && board[nr][nf] === knightChar) {
attackers.push({ r: nr, f: nf, piece: 'n', value: pv.n });
}
}
// Sliding pieces: bishop/queen (diagonals), rook/queen (straights)
const bishopChar = color === 'w' ? 'B' : 'b';
const rookChar = color === 'w' ? 'R' : 'r';
const queenChar = color === 'w' ? 'Q' : 'q';
// Diagonals (bishop + queen)
for (const [dr, df] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
for (let dist = 1; dist < 8; dist++) {
const sr = rank + dr * dist, sf = file + df * dist;
if (!inBounds(sr, sf)) break;
const p = board[sr][sf];
if (p) {
if (p === bishopChar || p === queenChar) {
attackers.push({ r: sr, f: sf, piece: p.toLowerCase(), value: pv[p.toLowerCase()] });
}
break; // blocked
}
}
}
// Straights (rook + queen)
for (const [dr, df] of [[-1,0],[1,0],[0,-1],[0,1]]) {
for (let dist = 1; dist < 8; dist++) {
const sr = rank + dr * dist, sf = file + df * dist;
if (!inBounds(sr, sf)) break;
const p = board[sr][sf];
if (p) {
if (p === rookChar || p === queenChar) {
attackers.push({ r: sr, f: sf, piece: p.toLowerCase(), value: pv[p.toLowerCase()] });
}
break; // blocked
}
}
}
// King attacks
const kingChar = color === 'w' ? 'K' : 'k';
for (let dr = -1; dr <= 1; dr++) {
for (let df = -1; df <= 1; df++) {
if (dr === 0 && df === 0) continue;
const kr = rank + dr, kf = file + df;
if (inBounds(kr, kf) && board[kr][kf] === kingChar) {
attackers.push({ r: kr, f: kf, piece: 'k', value: pv.k });
}
}
}
return attackers;
},
// Returns centipawn value of the piece on a given square (from FEN)
getPieceValueFromFen: (fen, sq) => {
if (!fen || !sq || sq.length < 2) return 0;
const piece = WeaknessProfile._identifyPiece(fen, sq);
const values = { pawn: 100, knight: 300, bishop: 320, rook: 500, queen: 900, king: 99999 };
return values[piece] || 0;
},
// Check if a move is safe — will the moved piece be hanging on its destination?
// Returns true if safe, false if the piece would be lost or a bad trade
isMoveSafe: (move, fen) => {
if (!move || move.length < 4) return true;
// --- Method 1: If this move is in the engine PV, check eval + board safety ---
const candidates = State.candidates;
const bestEval = candidates[1]?.eval;
let inPV = false;
for (let i = 1; i <= CONFIG.multiPV; i++) {
const c = candidates[i];
if (!c?.move || !c?.eval) continue;
if (c.move === move) {
if (bestEval?.type === 'cp' && c.eval.type === 'cp') {
const cpLoss = (bestEval.value - c.eval.value) * 100;
if (cpLoss > 250) return false;
}
if (c.eval.type === 'mate' && c.eval.value < 0) return false;
inPV = true;
break; // don't return yet — still check board for high-value pieces
}
}
// --- Method 2: Board-level attack analysis ---
// Always run for non-PV moves. For PV moves, only run if a queen or rook is moving
// (engine might say a queen move is "only -200cp" due to compensation, but visually
// hanging your queen looks terrible and is a dead giveaway)
try {
const fromSq = move.substring(0, 2);
const movingValue = HumanStrategy.getPieceValueFromFen(fen, fromSq);
// If it's a PV move and NOT a high-value piece, trust the engine
if (inPV && movingValue < 500) return true;
// For high-value PV moves (queen/rook) or any non-PV move: run full board analysis
const board = HumanStrategy._parseFenBoard(fen);
const sideToMove = fen.split(' ')[1] || 'w';
const enemyColor = sideToMove === 'w' ? 'b' : 'w';
const toSq = move.substring(2, 4);
const [toR, toF] = HumanStrategy._sqToIdx(toSq);
const [fromR, fromF] = HumanStrategy._sqToIdx(fromSq);
const movingPieceChar = board[fromR]?.[fromF];
if (!movingPieceChar) return true; // can't identify piece
// Is there an enemy piece on the destination? (capture)
const capturedChar = board[toR]?.[toF];
const capturedValue = capturedChar ? (HumanStrategy._pieceValues[capturedChar.toLowerCase()] || 0) : 0;
// Simulate the move on the board for attack detection
const simBoard = board.map(row => [...row]);
simBoard[fromR][fromF] = null;
simBoard[toR][toF] = movingPieceChar;
// Who attacks the destination AFTER the move?
const enemyAttackers = HumanStrategy._getAttackers(simBoard, toR, toF, enemyColor);
const friendlyDefenders = HumanStrategy._getAttackers(simBoard, toR, toF, sideToMove);
// If no enemy attacks the square, it's safe
if (enemyAttackers.length === 0) return true;
// If it's a capture and we win material even if they recapture, it's fine
// e.g. knight takes undefended rook — even if enemy recaptures we traded 300 for 500
if (capturedValue >= movingValue) return true;
// Enemy attacks the square — check if we have enough defenders
if (friendlyDefenders.length === 0) {
// Piece is hanging with no defenders — BAD
// Allow it only if the piece is a pawn (losing 100cp is minor)
if (movingValue <= 100) return true;
Utils.log(`PieceProtect: ${move} hangs ${movingPieceChar} (${movingValue}cp) with no defenders`, 'debug');
return false;
}
// Both sides attack — do simple static exchange evaluation (SEE)
// Sort attackers by value (cheapest first, like real exchanges)
const atkSorted = [...enemyAttackers].sort((a, b) => a.value - b.value);
const defSorted = [...friendlyDefenders].sort((a, b) => a.value - b.value);
// Simulate exchange: enemy captures first, then we recapture, etc.
let materialOnSquare = movingValue; // our piece is there
let balance = 0; // net material change from our perspective
let turn = 0; // 0 = enemy captures, 1 = we recapture
let atkIdx = 0, defIdx = 0;
while (true) {
if (turn % 2 === 0) {
// Enemy captures
if (atkIdx >= atkSorted.length) break; // enemy can't capture
balance -= materialOnSquare; // we lose the piece on the square
materialOnSquare = atkSorted[atkIdx].value; // enemy piece now sits there
atkIdx++;
} else {
// We recapture
if (defIdx >= defSorted.length) break; // we can't recapture
balance += materialOnSquare; // we take their piece
materialOnSquare = defSorted[defIdx].value; // our piece now sits there
defIdx++;
}
turn++;
// If it's the enemy's turn and balance is already positive for us, they'd stop
if (turn % 2 === 0 && balance > 0) break;
// If it's our turn and balance is very negative, we'd stop
if (turn % 2 === 1 && balance < -movingValue) break;
}
// Reject threshold depends on piece value:
// Queen/Rook: reject if losing ANY material (> 50cp, to allow rounding)
// Minor pieces: reject if losing more than a pawn (> 120cp)
// Pawns: always ok (losing a pawn is minor)
const rejectThreshold = movingValue >= 500 ? -50 : -120;
if (balance < rejectThreshold) {
Utils.log(`PieceProtect: ${move} loses ~${Math.abs(balance)}cp in exchange (${movingPieceChar} worth ${movingValue}cp vs ${atkSorted.length} attackers, ${defSorted.length} defenders)`, 'debug');
return false;
}
return true;
} catch (e) {
// If analysis fails, fall back to conservative: don't move high-value pieces
const fromSq = move.substring(0, 2);
const val = HumanStrategy.getPieceValueFromFen(fen, fromSq);
return val < 500; // only allow pawns/knights/bishops as fallback
}
},
selectMove: (fen, bestMove, isBook) => {
if (isBook || !CONFIG.humanization.enabled) {
return { move: bestMove, reason: isBook ? 'book' : 'engine', isBest: true };
}
// Repertoire-hard override (only on move 1 of each color)
try {
const g = Game.getBoardGame();
const legal = g && g.getLegalMoves ? g.getLegalMoves().map(m =>
(m.from && m.to) ? m.from + m.to + (m.promotion || '') : (typeof m === 'string' ? m : null)
).filter(Boolean) : [];
if (legal.length) {
const rep = Account.repertoireMove(fen, legal);
if (rep && rep !== bestMove) {
Utils.log(`Repertoire: forcing ${rep} (engine preferred ${bestMove})`, 'info');
return { move: rep, reason: 'repertoire', isBest: false };
}
}
} catch (e) { /* non-fatal */ }
// Messy-resign: blunder ONE move before resigning, if scheduled
if (State.human.resignBlunderNext) {
const bl = HumanStrategy.pickBlunderMove();
if (bl) {
State.human.resignBlunderNext = false; // consume the roll
Utils.log('MessyResign: playing blunder move before resign', 'warn');
return { move: bl, reason: 'resign-blunder', isBest: false };
}
}
// Update accuracy cluster
HumanStrategy.updateCluster();
// --- PRE-DECIDE think time category (used later by calculateDelay) ---
// This drives timing-accuracy coupling: we decide HOW LONG we'll think FIRST,
// then that influences move quality
const tac = CONFIG.humanization.timingAccuracyCoupling;
if (tac.enabled) {
const r = Math.random();
if (r < 0.25) State.human.thinkCategory = 'fast';
else if (r < 0.70) State.human.thinkCategory = 'normal';
else State.human.thinkCategory = 'slow';
}
// --- ANTI-CORRELATION POISONING ---
// Skip when losing — accuracy matters more than stealth when behind
const ac = CONFIG.humanization.antiCorrelation;
const isLosing = State.currentEval && State.currentEval.type === 'cp' && State.currentEval.value < -1.0;
if (ac.enabled && State.moveCount > 3 && !isLosing) {
// Track top move rate and enforce hard cap
const topRate = State.human.totalMoveCount > 0
? State.human.topMoveCount / State.human.totalMoveCount : 0;
// A) Miss small tactics sometimes
if (State.currentEval && State.candidates[1]?.eval) {
const prevEval = State.human._prevEval || 0;
const evalJump = State.currentEval.value - prevEval;
if (evalJump > 0.3 && evalJump < ac.missSmallTacticThreshold) {
if (Math.random() < ac.missSmallTacticRate) {
// Play a "safe" alternative instead of the tactic
const safeMove = HumanStrategy.pickSuboptimalMove(fen);
if (safeMove && HumanStrategy.isMoveSafe(safeMove, fen)) {
Utils.log(`AntiCorr: Missed small tactic (eval jump ${evalJump.toFixed(2)}), playing safe`, 'debug');
return { move: safeMove, reason: 'missed-tactic', isBest: false };
}
}
}
}
// B) When SF#2/3 are close in eval, prefer them sometimes
if (Object.keys(State.candidates).length >= 2) {
const best = State.candidates[1];
for (let i = 2; i <= Math.min(3, CONFIG.multiPV); i++) {
const alt = State.candidates[i];
if (!alt?.eval || !best?.eval) continue;
if (best.eval.type !== 'cp' || alt.eval.type !== 'cp') continue;
const diff = Math.abs(best.eval.value - alt.eval.value);
if (diff <= ac.closeEvalThreshold) {
// These moves are essentially equal — sometimes pick the alternative
let preferRate = ac.closeEvalPreferRate;
// If we're over the top move cap, strongly prefer alternatives
if (topRate > ac.maxTopMoveRate) preferRate += 0.25;
if (Math.random() < preferRate) {
Utils.log(`AntiCorr: SF#${i} within ${(diff*100).toFixed(0)}cp, playing ${alt.move} instead of ${best.move}`, 'debug');
return { move: alt.move, reason: 'close-alt', isBest: false, isCloseAlt: true };
}
}
}
}
// C) Hard cap enforcement: if top move rate is too high, force a non-best move
if (topRate > ac.maxTopMoveRate && State.human.totalMoveCount >= 10) {
const subMove = HumanStrategy.pickSuboptimalMove(fen);
if (subMove && HumanStrategy.isMoveSafe(subMove, fen)) {
Utils.log(`AntiCorr: Top move rate ${(topRate*100).toFixed(0)}% > cap ${(ac.maxTopMoveRate*100).toFixed(0)}%, forcing alt`, 'debug');
return { move: subMove, reason: 'correlation-cap', isBest: false };
}
}
}
// --- TIMING-ACCURACY COUPLING ---
// Fast think = more errors, slow think = fewer errors (with noise)
let couplingSuboptBoost = 0;
let couplingBestBoost = 0;
if (tac.enabled) {
const isNoisy = Math.random() < tac.noiseRate; // occasional inversion
if (State.human.thinkCategory === 'fast' && !isNoisy) {
couplingSuboptBoost = tac.fastMoveSuboptimalBoost;
} else if (State.human.thinkCategory === 'slow' && !isNoisy) {
couplingBestBoost = tac.slowMoveBestBoost;
} else if (State.human.thinkCategory === 'fast' && isNoisy) {
// "Fast good move" — pattern recognition hit
couplingBestBoost = 0.10;
} else if (State.human.thinkCategory === 'slow' && isNoisy) {
// "Slow bad move" — overthinking
couplingSuboptBoost = 0.07;
}
}
// --- WEAKNESS PROFILE ---
const weaknessExtra = WeaknessProfile.getExtraErrorRate(fen, bestMove);
// --- PLAYER MOVE DB DIVERSIFICATION ---
// Skip when losing — play engine moves to fight back
const pdb = CONFIG.humanization.playerMoveDB;
if (pdb.enabled && State.moveCount > 4 && !isLosing) {
const cacheKey = fen.split(' ').slice(0, 4).join(' ');
const playerMoves = State.playerDBCache.get(cacheKey);
if (playerMoves && playerMoves.length > 0 && Math.random() < pdb.preferRate) {
const dbMove = PlayerMoveDB.pickMove(playerMoves);
if (dbMove && dbMove !== bestMove) {
// Only use if it's not catastrophically bad — check if it's in PV or at least legal
const inPV = Object.values(State.candidates).some(c => c?.move === dbMove);
if (inPV && HumanStrategy.isMoveSafe(dbMove, fen)) {
Utils.log(`PlayerDB: Playing human move ${dbMove} (from ${playerMoves.length} options at target rating)`, 'debug');
return { move: dbMove, reason: 'player-db', isBest: dbMove === bestMove };
}
// Even if not in PV, if it's the most popular human move, trust it — but still check safety
const topDBMove = playerMoves.reduce((a, b) => a.games > b.games ? a : b);
if (dbMove === topDBMove.uci && topDBMove.games >= 20 && HumanStrategy.isMoveSafe(dbMove, fen)) {
Utils.log(`PlayerDB: Playing top human move ${dbMove} (${topDBMove.games} games)`, 'debug');
return { move: dbMove, reason: 'player-db', isBest: false };
}
}
}
}
// --- RANDOM LEGAL MOVE (anti-detection) ---
if (CONFIG.antiDetection.randomLegalMoveChance > 0 && State.moveCount > 5) {
if (Math.random() < CONFIG.antiDetection.randomLegalMoveChance) {
const randomMove = HumanStrategy.pickRandomLegalMove(fen);
if (randomMove && HumanStrategy.isMoveSafe(randomMove, fen)) {
return { move: randomMove, reason: 'random-legal', isBest: false };
}
}
}
// --- BLUNDER CHECK (very rare) ---
if (HumanStrategy.shouldBlunder(fen)) {
const blunderMove = HumanStrategy.pickBlunderMove();
if (blunderMove && HumanStrategy.isMoveSafe(blunderMove, fen)) {
return { move: blunderMove, reason: 'blunder', isBest: false };
}
}
// --- SUBOPTIMAL MOVE CHECK ---
// Apply coupling and weakness boosts to the suboptimal decision
if (HumanStrategy.shouldPlaySuboptimal(fen, couplingSuboptBoost + weaknessExtra)) {
// If best move would be forced by coupling slow-think boost, override
if (couplingBestBoost > 0 && Math.random() < couplingBestBoost) {
Utils.log('TimingCoupling: Slow think → playing best despite suboptimal trigger', 'debug');
return { move: bestMove, reason: 'best', isBest: true };
}
const subMove = HumanStrategy.pickSuboptimalMove(fen);
if (subMove && HumanStrategy.isMoveSafe(subMove, fen)) {
return { move: subMove, reason: 'suboptimal', isBest: false };
}
}
return { move: bestMove, reason: 'best', isBest: true };
},
trackMove: (isBest, moveResult) => {
State.human.totalMoveCount++;
if (isBest) {
State.human.bestMoveCount++;
State.human.topMoveCount++;
State.human.perfectStreak++;
State.human.sloppyStreak = 0;
} else {
// Close alternatives still count toward top move for correlation tracking
// (Chess.com sees them as essentially engine moves too)
if (moveResult?.isCloseAlt) State.human.topMoveCount++;
State.human.perfectStreak = 0;
State.human.sloppyStreak++;
}
State.human.lastMoveWasBest = isBest;
// Store eval for next move's tactic detection
State.human._prevEval = State.currentEval?.value || 0;
},
calculateDelay: (fen, moveResult, isBook) => {
const t = CONFIG.timing;
const personality = State.human.gamePersonality || { timingMult: 1 };
const phase = HumanStrategy.getGamePhase(fen);
let min, max;
// ============================================================
// LAYER 0: Premove simulation (instant — bypasses everything)
//
// Gated by CONFIG.premoveGating. Real players premove ONLY when the
// opponent's reply is effectively forced — single legal response,
// obvious recapture, or king-moves-out-of-check. Premoving on any
// "predicted" reply is a bot tell because it happens too often in
// non-forcing positions.
// ============================================================
if (t.premove.enabled && State.human.predictedReply && moveResult.isBest) {
if (Math.random() < t.premove.chance && State.moveCount > 5) {
const gate = CONFIG.premoveGating;
let allowPremove = !gate.enabled; // if gating disabled, always allow
if (gate.enabled) {
// Check: is opponent reply forced (single legal move)?
let opponentHasSingleReply = false;
try {
const g = Game.getBoardGame();
// We'd need to see the position AFTER our move. Approximation:
// use predictedReplyFen (we stored the fen BEFORE our move).
// Without a move generator that can push/pop, we rely on the
// engine's signal: if MultiPV gave us a strongly-dominant
// single top reply, treat it as effectively forced.
const cands = State.candidates || {};
const scores = Object.values(cands).map(c => c.score || 0);
if (scores.length >= 2) {
scores.sort((a, b) => b - a);
// Dominance: top is >200cp better than 2nd (or mate)
if ((scores[0] - scores[1]) > 200) opponentHasSingleReply = true;
}
} catch (e) {}
if (gate.forcedOnly && opponentHasSingleReply) allowPremove = true;
// Recapture gate: our move was a capture AND the predicted
// reply is also a capture on the same destination square.
if (!allowPremove && gate.allowRecaptures) {
const ourMove = moveResult.move || '';
const replyMove = State.human.predictedReply || '';
if (ourMove.length >= 4 && replyMove.length >= 4) {
const ourTo = ourMove.substring(2, 4);
const replyTo = replyMove.substring(2, 4);
if (Game.isCapture(ourMove) && ourTo === replyTo) {
allowPremove = true;
}
}
}
}
if (allowPremove) {
Utils.log(`Timing: Premove (gated: ${CONFIG.premoveGating.enabled ? 'pass' : 'disabled'})`, 'debug');
return Utils.humanDelay(t.premove.delay.min, t.premove.delay.max);
} else {
Utils.log('Timing: Premove suppressed by gating (position not forcing enough)', 'debug');
}
}
}
// ============================================================
// LAYER 0B: Recapture speed (item 6)
// When opponent just captured and our best move is to recapture,
// humans respond almost instantly — it's the most obvious move.
// ============================================================
if (State.lastMoveWasCapture && Game.isCapture(moveResult.move) && moveResult.isBest && !isBook) {
min = 300; max = 1100;
Utils.log('Timing: Instant recapture', 'debug');
// Still apply clock awareness to recaptures
if (t.clockAware.enabled && State.clock.myTime != null) {
for (const threshold of t.clockAware.thresholds) {
if (State.clock.myTime <= threshold.secondsBelow) {
min *= threshold.timingMult;
max *= threshold.timingMult;
break;
}
}
}
const recapDelay = Utils.humanDelay(min, max);
State.recentTimings.push(recapDelay);
if (State.recentTimings.length > 20) State.recentTimings.shift();
return recapDelay;
}
// ============================================================
// LAYER 1: Base timing by move type
// ============================================================
if (isBook) {
min = t.book.min;
max = t.book.max;
} else if (State.moveCount <= 6) {
min = t.earlyGame.min;
max = t.earlyGame.max;
} else if (Game.isCapture(moveResult.move) && State.currentEval && Math.abs(State.currentEval.value) > 2) {
min = t.forced.min;
max = t.forced.max;
} else if (Math.random() < t.instantMove.chance && State.moveCount > 5) {
min = t.instantMove.min;
max = t.instantMove.max;
Utils.log('Timing: Instant/pre-move');
} else if (Math.random() < t.longThink.chance) {
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;
}
if (State.currentEval && Math.abs(State.currentEval.value) < 0.3) {
min += 500;
max += 1200;
}
if (moveResult.reason === 'suboptimal') {
min += 200;
max += 600;
}
if (moveResult.reason === 'blunder') {
if (Math.random() < 0.5) {
min = t.forced.min;
max = t.forced.max + 300;
} else {
min = t.complex.min;
max = t.complex.max;
}
}
}
// ============================================================
// LAYER 2: Time management curve (item 3)
// Real players spend ~15% of time in opening, ~55% middlegame, ~30% endgame
// This shapes the base timing to match that distribution.
// ============================================================
if (!isBook) {
if (phase === 'opening') {
min *= 0.65; max *= 0.75; // play faster in opening (known territory)
} else if (phase === 'middlegame') {
min *= 1.15; max *= 1.30; // think most in middlegame (critical decisions)
} else if (phase === 'endgame') {
// Endgame: a bit faster than middlegame but slower than opening
// (technique phase — fewer choices but need precision)
min *= 0.85; max *= 0.95;
}
}
// ============================================================
// LAYER 3: Opponent-move surprise reaction (item 1)
// After a surprising opponent move, think longer.
// After an expected/obvious move, respond faster.
// ============================================================
if (!isBook && State.moveCount > 3) {
const surprise = State.human.opponentMoveSurprise; // 0-1
if (surprise > 0.5) {
// Surprising move — need extra time to recalculate
const surpriseMult = 1.0 + surprise * 0.6; // up to 1.6x
min *= surpriseMult;
max *= surpriseMult;
Utils.log(`Timing: Surprised by opponent move (${(surprise*100).toFixed(0)}%) → ${surpriseMult.toFixed(2)}x`, 'debug');
} else if (surprise < 0.1 && State.human.predictedReply) {
// Completely expected move — respond a bit faster
min *= 0.80; max *= 0.85;
}
}
// ============================================================
// LAYER 4: Think momentum / inertia (item 2)
// After a long think, the next move is often faster because
// you already calculated the continuation during the long think.
// After a fast move, the next might be slower (didn't plan ahead).
// ============================================================
if (!isBook && State.human.lastThinkTime > 0) {
const lastThink = State.human.lastThinkTime;
if (lastThink > 8000) {
// Last move was a long think — this one should be faster (calculated ahead)
min *= 0.55; max *= 0.70;
Utils.log('Timing: Momentum — fast follow-up after long think', 'debug');
} else if (lastThink > 5000) {
min *= 0.75; max *= 0.85;
} else if (lastThink < 1200 && State.moveCount > 8) {
// Last move was very fast — might need to slow down and actually think now
min *= 1.10; max *= 1.25;
}
}
// ============================================================
// LAYER 5: 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;
}
// ============================================================
// LAYER 6: Clock awareness
// ============================================================
if (t.clockAware.enabled && State.clock.myTime != null) {
for (const threshold of t.clockAware.thresholds) {
if (State.clock.myTime <= threshold.secondsBelow) {
min *= threshold.timingMult;
max *= threshold.timingMult;
break;
}
}
}
// ============================================================
// LAYER 7: Per-game personality jitter + persistent player tempo (item 5 partial)
// ============================================================
min *= personality.timingMult;
max *= personality.timingMult;
// Persistent player tempo: some accounts are naturally fast/slow players
min *= State.human.playerTempo;
max *= State.human.playerTempo;
// Tilt: thinking slower/frustrated overthinking after a loss
if (State.human.tiltActive && CONFIG.tilt?.enabled) {
min *= CONFIG.tilt.timingMult;
max *= CONFIG.tilt.timingMult;
}
// ============================================================
// LAYER 8: Timing-accuracy coupling (existing)
// ============================================================
if (CONFIG.humanization.timingAccuracyCoupling.enabled && !isBook) {
const cat = State.human.thinkCategory;
if (cat === 'fast') {
min *= 0.45; max *= 0.55;
Utils.log('TimingCoupling: Fast think', 'debug');
} else if (cat === 'slow') {
min *= 1.4; max *= 1.8;
Utils.log('TimingCoupling: Slow think', 'debug');
}
}
// ============================================================
// LAYER 9: Eval-aware timing (existing)
// ============================================================
if (State.currentEval && !isBook) {
const ev = State.currentEval.value;
if (Math.abs(ev) < 0.5) {
min *= 1.15; max *= 1.25;
} else if (ev > 3.0) {
min *= 0.7; max *= 0.8;
} else if (ev < -2.0) {
if (Math.random() < 0.4) {
min *= 0.5; max *= 0.65;
} else {
min *= 1.2; max *= 1.5;
}
}
}
// ============================================================
// CUMULATIVE MULTIPLIER DAMPENING
// ============================================================
// Layers 2-9 multiply min/max cumulatively. In worst case the total
// multiplier reaches ~5-8x which then hits the hard ceiling every
// time, creating a detectable "many moves at exactly max" cluster.
// Dampen the cumulative multiplier using sqrt-compression when it
// goes beyond 2.0x (or below 0.5x), so extreme stacks pull toward
// the middle while still preserving per-layer intent.
const baseMean = (t.base.min + t.base.max) / 2;
const currMean = (min + max) / 2;
const cumMult = baseMean > 0 ? currMean / baseMean : 1;
if (cumMult > 2.0) {
// sqrt-compress: 4x becomes 2x, 9x becomes 3x, 16x becomes 4x
const damped = Math.sqrt(cumMult * 2.0);
const scale = damped / cumMult;
min *= scale; max *= scale;
Utils.log(`Timing: Dampened cumulative ${cumMult.toFixed(2)}x -> ${damped.toFixed(2)}x`, 'debug');
} else if (cumMult < 0.5) {
// Same compression for sub-0.5x stacks (prevents absurdly fast plays)
const damped = 0.5 / Math.sqrt(0.5 / cumMult);
const scale = damped / cumMult;
min *= scale; max *= scale;
}
// ============================================================
// FINAL: Sequence variation + clamp
// ============================================================
let delay = Utils.humanDelay(min, max);
if (t.sequenceVariation.enabled && State.recentTimings.length >= t.sequenceVariation.windowSize) {
const recent = State.recentTimings.slice(-t.sequenceVariation.windowSize);
const mean = recent.reduce((a, b) => a + b, 0) / recent.length;
const variance = recent.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / recent.length;
const stdDev = Math.sqrt(variance);
const cv = mean > 0 ? stdDev / mean : 0;
if (cv < t.sequenceVariation.similarityThreshold) {
if (Math.random() < 0.5) {
delay = Utils.humanDelay(min * 0.3, min * 0.6);
Utils.log('Timing: Forced fast outlier (sequence variation)', 'debug');
} else {
delay = Utils.humanDelay(max * 1.15, max * 1.8);
Utils.log('Timing: Forced slow outlier (sequence variation)', 'debug');
}
}
}
// Hard clamp: respect user-configured base max.
// Use a softer, noisier ceiling so we don't produce a spike at exactly
// 1.5x base.max across every long-think move.
const userMax = t.base.max;
const noisyCeiling = userMax * (1.35 + Math.random() * 0.30); // 1.35-1.65x
if (delay > noisyCeiling) {
Utils.log(`Timing: Clamped ${Math.round(delay)}ms -> ${Math.round(noisyCeiling)}ms`, 'debug');
delay = noisyCeiling;
}
// Minimum floor: 250ms unless it's an explicit premove/instant/recapture case.
// 150ms was inhumanly fast — nobody makes non-recapture decisions that quick.
delay = Math.max(delay, 250);
// Time-scramble override: if < 5 seconds left, NEVER think more than 600ms
if (State.clock.myTime != null && State.clock.myTime < 5) {
delay = Math.min(delay, 400 + Math.random() * 200);
}
State.recentTimings.push(delay);
if (State.recentTimings.length > 20) State.recentTimings.shift();
return delay;
},
};
// ═══════════════════════════════════════════
// HUMANIZER (Mouse simulation & move execution)
// ═══════════════════════════════════════════
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 });
},
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;
// Hardware persona shapes the drag character:
// trackpad -> slower, noisier, longer click-hold
// mouse -> baseline
// tablet -> medium noise, slow click-hold
const persona = Account.currentPersona() || { jitterScale: 1, clickHoldMs: { min: 50, max: 110 }, speedScale: 1 };
const jScale = persona.jitterScale;
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 };
const pickupNoise = () => Utils.gaussianRandom(0, 2 * jScale);
const sx = startPos.x + pickupNoise();
const sy = startPos.y + pickupNoise();
// pointerType advertises what device the "user" is on. Trackpads still
// register as 'mouse' in browser API but some sites sniff this; we keep
// it as 'mouse' for all personas (trackpad is a mouse device to the DOM).
const realisticPointerProps = (x, y, prevX, prevY) => ({
width: 1,
height: 1,
pressure: 0.5 + Math.random() * 0.25,
tangentialPressure: 0,
tiltX: Math.round(Utils.gaussianRandom(0, 3 * jScale)),
tiltY: Math.round(Utils.gaussianRandom(0, 3 * jScale)),
twist: 0,
pointerType: 'mouse',
movementX: prevX != null ? Math.round(x - prevX) : 0,
movementY: prevY != null ? Math.round(y - prevY) : 0,
});
targetSource.dispatchEvent(new PointerEvent('pointerover', { ...opts, ...realisticPointerProps(sx, sy), clientX: sx, clientY: sy }));
targetSource.dispatchEvent(new PointerEvent('pointerdown', { ...opts, ...realisticPointerProps(sx, sy), clientX: sx, clientY: sy }));
targetSource.dispatchEvent(new MouseEvent('mousedown', { ...opts, clientX: sx, clientY: sy }));
// Click-hold time is persona-specific (trackpad/tablet hold longer).
const spd = (CONFIG.dragSpeed || 1.0) / persona.speedScale;
const clickHold = Utils.randomRange(persona.clickHoldMs.min, persona.clickHoldMs.max);
await Utils.sleep(clickHold);
// Helper: run a noisy human-like drag path between two points.
// IMPORTANT: we dispatch pointermove to the SAME element that received
// pointerdown (`targetSource`) whenever possible. This preserves the
// implicit pointer-capture contract Chess.com's drag handler expects.
// Dispatching to `document` breaks that contract and leaves a detectable
// gap in the pointer event target chain.
const bezierPath = async (from, to, stepCount, speedMult = 1) => {
const pdx = to.x - from.x, pdy = to.y - from.y;
const pDist = Math.sqrt(pdx * pdx + pdy * pdy);
if (pDist < 1) return;
// Multiple random control points for a wobbly spline, not a clean curve
const perpX = -pdy / pDist, perpY = pdx / pDist;
const cp1t = 0.25 + Math.random() * 0.15;
const cp2t = 0.55 + Math.random() * 0.15;
const wobble1 = Utils.gaussianRandom(0, pDist * 0.18 * jScale);
const wobble2 = Utils.gaussianRandom(0, pDist * 0.14 * jScale);
const cp1 = { x: from.x + pdx * cp1t + perpX * wobble1, y: from.y + pdy * cp1t + perpY * wobble1 };
const cp2 = { x: from.x + pdx * cp2t + perpX * wobble2, y: from.y + pdy * cp2t + perpY * wobble2 };
// Cubic bezier eval
const cubicBez = (a, b, c, d, t) => {
const omt = 1 - t;
return omt*omt*omt*a + 3*omt*omt*t*b + 3*omt*t*t*c + t*t*t*d;
};
// Wobble state that drifts smoothly (fake Perlin)
let wobX = 0, wobY = 0;
const wobDrift = () => {
wobX += Utils.gaussianRandom(0, 1.2 * jScale);
wobY += Utils.gaussianRandom(0, 1.2 * jScale);
wobX *= 0.7; wobY *= 0.7; // dampen so it doesn't run away
};
const totalSteps = Math.max(stepCount, Math.round(pDist / 6));
let lastPauseAt = 0;
for (let i = 1; i <= totalSteps; i++) {
const t = i / totalSteps;
// Base position from cubic bezier
let cx_ = cubicBez(from.x, cp1.x, cp2.x, to.x, t);
let cy_ = cubicBez(from.y, cp1.y, cp2.y, to.y, t);
// Perpendicular wobble — stronger in the middle, fades at endpoints
wobDrift();
const wobbleEnvelope = Math.sin(t * Math.PI) * 1.5;
cx_ += wobX * wobbleEnvelope;
cy_ += wobY * wobbleEnvelope;
// Random high-freq noise (hand tremor)
const tremor = Math.max(0.3, (1 - t) * 2.5 + Math.sin(t * 12) * 0.5) * jScale;
cx_ += Utils.gaussianRandom(0, tremor);
cy_ += Utils.gaussianRandom(0, tremor);
const rpp = realisticPointerProps(cx_, cy_, prevMoveX, prevMoveY);
targetSource.dispatchEvent(new PointerEvent('pointermove', { ...opts, ...rpp, clientX: cx_, clientY: cy_ }));
targetSource.dispatchEvent(new MouseEvent('mousemove', { ...opts, clientX: cx_, clientY: cy_, movementX: rpp.movementX, movementY: rpp.movementY }));
prevMoveX = cx_;
prevMoveY = cy_;
// Speed: slow start, fast middle, slow end (bell curve)
const bell = Math.sin(t * Math.PI);
const baseDelay = Utils.randomRange(6, 18) * (1.4 - bell * 0.9);
const delay = Math.max(3, Math.round(baseDelay * speedMult * spd));
// Most steps get a delay, but vary the chance
if (Math.random() < 0.70) await Utils.sleep(delay);
// Occasional micro-pause (human recalculating / hand jitter)
if (t > 0.15 && t < 0.85 && (t - lastPauseAt) > 0.2 && Math.random() < 0.08) {
await Utils.sleep(Utils.randomRange(30, 80) * spd);
lastPauseAt = t;
}
}
};
const dx = endPos.x - startPos.x;
const dy = endPos.y - startPos.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const sqSize = board.getBoundingClientRect().width / 8;
let prevMoveX = sx, prevMoveY = sy;
// --- CHANGE-OF-MIND FAKE-OUT ---
const com = CONFIG.antiDetection.changeOfMind;
const doFakeout = com.enabled && Math.random() < com.chance && dist > sqSize * 1.2;
if (doFakeout) {
// Pick a fake target: a square adjacent to the real target but NOT the real target
const offsets = [[-1,0],[1,0],[0,-1],[0,1],[-1,-1],[1,1],[-1,1],[1,-1]];
const realFile = endPos.x, realRank = endPos.y;
const pick = offsets[Math.floor(Math.random() * offsets.length)];
const fakeX = endPos.x + pick[0] * sqSize;
const fakeY = endPos.y + pick[1] * sqSize;
// Clamp to board bounds
const bRect = board.getBoundingClientRect();
const clampX = Math.max(bRect.left + sqSize * 0.5, Math.min(bRect.right - sqSize * 0.5, fakeX));
const clampY = Math.max(bRect.top + sqSize * 0.5, Math.min(bRect.bottom - sqSize * 0.5, fakeY));
const fakePos = { x: clampX, y: clampY };
// Phase 1: drag toward the fake square (go ~75-90% of the way)
const fakeSteps = Math.max(6, Math.min(14, Math.round(dist / 10)));
const approach = 0.75 + Math.random() * 0.15;
const nearFake = {
x: startPos.x + (fakePos.x - startPos.x) * approach,
y: startPos.y + (fakePos.y - startPos.y) * approach
};
await bezierPath(startPos, nearFake, fakeSteps, 1.0);
// Phase 2: slow down near the fake square (decelerating micro-movements)
const slowSteps = Math.round(Utils.randomRange(2, 5));
for (let i = 0; i < slowSteps; i++) {
const driftX = prevMoveX + Utils.gaussianRandom(0, 3);
const driftY = prevMoveY + Utils.gaussianRandom(0, 3);
const rpp = realisticPointerProps(driftX, driftY, prevMoveX, prevMoveY);
targetSource.dispatchEvent(new PointerEvent('pointermove', { ...opts, ...rpp, clientX: driftX, clientY: driftY }));
targetSource.dispatchEvent(new MouseEvent('mousemove', { ...opts, clientX: driftX, clientY: driftY, movementX: rpp.movementX, movementY: rpp.movementY }));
prevMoveX = driftX;
prevMoveY = driftY;
await Utils.sleep(Utils.randomRange(25, 60));
}
// Phase 3: hesitate — hold still
const hesitate = Utils.humanDelay(com.hesitateMs.min, com.hesitateMs.max);
Utils.log(`Change-of-mind: faked toward (${pick[0]},${pick[1]}), hesitating ${Math.round(hesitate)}ms`, 'debug');
UI.toast('Fake-Out', `Changed mind mid-drag — redirecting to real target`, 'fakeout', 2500);
await Utils.sleep(hesitate);
// Phase 4: redirect to real target (slightly faster, more decisive)
const redirectSteps = Math.max(6, Math.min(12, Math.round(dist / 12)));
await bezierPath({ x: prevMoveX, y: prevMoveY }, endPos, redirectSteps, 0.7);
} else {
// Normal drag path
const steps = Math.max(8, Math.min(18, Math.round(dist / 8) + Math.round(Math.random() * 4)));
await bezierPath(startPos, endPos, steps, 1.0);
}
// Overshoot + settle — common in real mouse movement
if (Math.random() < 0.35) {
const ovMag = Utils.randomRange(3, 10);
const ovAngle = Math.random() * Math.PI * 2;
const ovX = endPos.x + Math.cos(ovAngle) * ovMag;
const ovY = endPos.y + Math.sin(ovAngle) * ovMag;
const rpp1 = realisticPointerProps(ovX, ovY, prevMoveX, prevMoveY);
targetSource.dispatchEvent(new PointerEvent('pointermove', { ...opts, ...rpp1, clientX: ovX, clientY: ovY }));
prevMoveX = ovX; prevMoveY = ovY;
await Utils.sleep(Utils.randomRange(10, 30) * spd);
// Correct back with a small wobble
const settleX = endPos.x + Utils.gaussianRandom(0, 1.5);
const settleY = endPos.y + Utils.gaussianRandom(0, 1.5);
const rpp2 = realisticPointerProps(settleX, settleY, prevMoveX, prevMoveY);
targetSource.dispatchEvent(new PointerEvent('pointermove', { ...opts, ...rpp2, clientX: settleX, clientY: settleY }));
prevMoveX = settleX; prevMoveY = settleY;
await Utils.sleep(Utils.randomRange(8, 20) * spd);
// Final settle on target
const rpp3 = realisticPointerProps(endPos.x, endPos.y, prevMoveX, prevMoveY);
targetSource.dispatchEvent(new PointerEvent('pointermove', { ...opts, ...rpp3, clientX: endPos.x, clientY: endPos.y }));
await Utils.sleep(Utils.randomRange(5, 15) * spd);
}
const toCoords = Game.squareToCoords(toSq);
const targetEl = board.querySelector(`.square-${toCoords}`) ||
document.elementFromPoint(endPos.x, endPos.y);
const dropTarget = targetEl || board;
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 }));
},
executeMove: async (move) => {
const currentFen = Game.getFen();
if (currentFen !== State.lastFen && State.moveCount > 1) return;
const from = move.substring(0, 2);
const to = move.substring(2, 4);
const promo = move.length > 4 ? move[4] : 'q';
const isPromotion = (to[1] === '8' || to[1] === '1') && Humanizer.isPawnMove(from);
Utils.log(`Auto-playing: ${from} -> ${to}${isPromotion ? '=' + promo : ''}`);
// Drag-only execution. We intentionally DO NOT fall back to the internal
// game.move() API: that bypasses the DOM, leaves no pointer telemetry, and
// is the single strongest signal Chess.com uses to flag scripted play.
// If the drag fails, the game simply doesn't move this turn — we'll re-try
// on the next gameLoop tick. Better a lost game than a banned account.
await Humanizer.dragDrop(from, to);
await Utils.sleep(180);
const afterFen = Game.getFen();
if (afterFen === currentFen) {
Utils.log(`Drag did not register for ${from}->${to}. NOT using game.move() fallback (too detectable). Will retry next tick.`, 'warn');
UI.toast('Drag Failed', `${from}\u2192${to} didn't register. Retrying safely.`, 'warn', 3500);
return;
}
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');
}
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}`);
let promoEl = null;
for (let i = 0; i < 20; i++) {
await Utils.sleep(100);
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;
}
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) {
promoEl = promo === 'q' ? pieces[0] : pieces[{ r: 1, b: 2, n: 3 }[promo] || 0];
}
}
}
if (promoEl) break;
}
if (promoEl) {
// Pick a slightly off-center hit point so chess.com's input stream
// sees a non-perfect tap (real fingers/mice never hit dead-center).
const rect = promoEl.getBoundingClientRect();
const jitter = (mag) => (Math.random() * 2 - 1) * mag;
const x = rect.left + rect.width / 2 + jitter(rect.width * 0.15);
const y = rect.top + rect.height / 2 + jitter(rect.height * 0.15);
const opts = {
bubbles: true, cancelable: true, composed: true,
buttons: 1, button: 0, pointerId: 2, pointerType: 'mouse', isPrimary: true,
pressure: 0.5, view: window
};
// Full natural sequence: pointerover -> pointerenter -> pointerdown
// -> mousedown -> (small hold) -> pointerup -> mouseup -> click.
// No raw .click() — it produces an untrusted synthetic event with no
// associated pointerdown/up history, which Chess.com's input audit
// can flag as scripted.
promoEl.dispatchEvent(new PointerEvent('pointerover', { ...opts, clientX: x, clientY: y, buttons: 0 }));
promoEl.dispatchEvent(new PointerEvent('pointerenter', { ...opts, clientX: x, clientY: y, buttons: 0 }));
promoEl.dispatchEvent(new MouseEvent('mouseover', { ...opts, clientX: x, clientY: y, buttons: 0 }));
promoEl.dispatchEvent(new MouseEvent('mouseenter', { ...opts, clientX: x, clientY: y, buttons: 0 }));
await Utils.sleep(Utils.randomRange(20, 60));
promoEl.dispatchEvent(new PointerEvent('pointerdown', { ...opts, clientX: x, clientY: y }));
promoEl.dispatchEvent(new MouseEvent('mousedown', { ...opts, clientX: x, clientY: y }));
await Utils.sleep(Utils.randomRange(40, 110));
promoEl.dispatchEvent(new PointerEvent('pointerup', { ...opts, clientX: x, clientY: y, buttons: 0 }));
promoEl.dispatchEvent(new MouseEvent('mouseup', { ...opts, clientX: x, clientY: y, buttons: 0 }));
promoEl.dispatchEvent(new MouseEvent('click', { ...opts, clientX: x, clientY: y, buttons: 0 }));
Utils.log(`Promotion: clicked ${pieceName}`);
} else {
Utils.log('Promotion dialog not found - default queen will be used', 'warn');
}
},
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 };
}
};
// ═══════════════════════════════════════════
// TELEMETRY NOISE GENERATOR
// ═══════════════════════════════════════════
const TelemetryNoise = {
_lastTick: 0,
tick: async () => {
const config = CONFIG.antiDetection.telemetryNoise;
if (!config || !config.enabled) return;
const now = Date.now();
// Only consider doing noise roughly once every 4 seconds to avoid looking frantic
if (now - TelemetryNoise._lastTick < 4000) return;
TelemetryNoise._lastTick = now;
const fen = Game.getFen();
if (!fen || Game.isMyTurn(fen) || State.isThinking) return;
const r = Math.random();
if (r < config.hoverChance) {
TelemetryNoise.spoofPondering();
} else if (r < config.hoverChance + config.premoveCancelChance) {
TelemetryNoise.spoofPremove();
} else if (r < config.hoverChance + config.premoveCancelChance + config.uiClickChance) {
TelemetryNoise.spoofUIClick();
}
},
spoofPondering: async () => {
const board = Game.getBoard();
if (!board) return;
const pieces = Array.from(board.querySelectorAll('.piece'));
if (pieces.length === 0) return;
const target = pieces[Math.floor(Math.random() * pieces.length)];
const rect = target.getBoundingClientRect();
if (rect.width === 0) return;
const x = rect.left + rect.width / 2 + Utils.gaussianRandom(0, 8);
const y = rect.top + rect.height / 2 + Utils.gaussianRandom(0, 8);
Utils.log('TelemetryNoise: Spoofing ponder hover', 'debug');
const opts = { bubbles: true, composed: true, pointerId: 1 };
board.dispatchEvent(new PointerEvent('pointermove', { ...opts, clientX: x, clientY: y }));
if (Math.random() < 0.3) {
await Utils.sleep(200);
board.dispatchEvent(new MouseEvent('contextmenu', { ...opts, clientX: x, clientY: y, button: 2, buttons: 2 }));
}
},
spoofPremove: async () => {
const board = Game.getBoard();
if (!board) return;
const myColor = State.playerColor || 'w';
const myPieces = Array.from(board.querySelectorAll(`.piece.${myColor}p, .piece.${myColor}n`));
if (myPieces.length === 0) return;
const startPiece = myPieces[Math.floor(Math.random() * myPieces.length)];
const sRect = startPiece.getBoundingClientRect();
if (sRect.width === 0) return;
const sx = sRect.left + sRect.width / 2;
const sy = sRect.top + sRect.height / 2;
Utils.log('TelemetryNoise: Spoofing canceled premove', 'debug');
const bRect = board.getBoundingClientRect();
const tx = sx + Utils.gaussianRandom(0, bRect.width / 4);
const ty = sy + (myColor === 'w' ? -bRect.width/4 : bRect.width/4);
// Borrow the bezier drag logic from Humanizer to make this look natural
const startPos = { x: sx, y: sy };
const endPos = { x: tx, y: ty };
// Manually dispatch the start of the drag
const opts = { bubbles: true, composed: true, pointerId: 1 };
startPiece.dispatchEvent(new PointerEvent('pointerdown', { ...opts, clientX: sx, clientY: sy, buttons: 1 }));
// Smooth bezier curve over ~10-15 steps
const dist = Math.sqrt((tx - sx) ** 2 + (ty - sy) ** 2);
const steps = Math.max(8, Math.round(dist / 10));
let prevMoveX = sx, prevMoveY = sy;
for (let i = 1; i <= steps; i++) {
const t = i / steps;
// Simple linear interpolation + slight arc for noise
const arc = Math.sin(t * Math.PI) * 15;
let cx = sx + (tx - sx) * t + arc;
let cy = sy + (ty - sy) * t + Utils.gaussianRandom(0, 2);
board.dispatchEvent(new PointerEvent('pointermove', {
...opts, clientX: cx, clientY: cy, buttons: 1,
movementX: Math.round(cx - prevMoveX),
movementY: Math.round(cy - prevMoveY),
}));
prevMoveX = cx; prevMoveY = cy;
await Utils.sleep(Utils.randomRange(10, 25));
}
await Utils.sleep(Utils.randomRange(150, 400));
// Cancel via right click
board.dispatchEvent(new MouseEvent('mousedown', { ...opts, clientX: prevMoveX, clientY: prevMoveY, button: 2, buttons: 2 }));
board.dispatchEvent(new PointerEvent('pointerup', { ...opts, clientX: prevMoveX, clientY: prevMoveY, buttons: 0 }));
},
spoofUIClick: async () => {
const safeSelectors = [
'.chat-scroll-area-component',
'.evaluation-bar-component',
'.clock-time-monospace',
'.player-avatar-component'
];
for (const sel of safeSelectors) {
const els = document.querySelectorAll(sel);
if (els.length > 0) {
const el = els[Math.floor(Math.random() * els.length)];
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
const x = rect.left + rect.width / 2 + Utils.gaussianRandom(0, 3);
const y = rect.top + rect.height / 2 + Utils.gaussianRandom(0, 3);
Utils.log(`TelemetryNoise: Spoofing UI click on ${sel.split('-')[0]}`, 'debug');
const opts = { bubbles: true, composed: true };
el.dispatchEvent(new MouseEvent('mousedown', { ...opts, clientX: x, clientY: y }));
el.dispatchEvent(new MouseEvent('mouseup', { ...opts, clientX: x, clientY: y }));
el.dispatchEvent(new MouseEvent('click', { ...opts, clientX: x, clientY: y }));
break;
}
}
}
}
};
// ═══════════════════════════════════════════
// MAIN LOOP
// ═══════════════════════════════════════════
const Main = {
_queueing: false,
_gameInstanceId: 0, // monotonically increases on every detected new game
_lastGameResultTracked: -1, // last instance id we credited to session stats
showWelcome: () => {
// Bumped key so the v15.1 first-run modal shows once even on accounts that
// already dismissed earlier versions.
const welcomeKey = 'ba_welcome_shown_v15_1';
if (GM_getValue(welcomeKey, false)) return;
GM_setValue(welcomeKey, true);
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 100000;
background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(4px); animation: fadeIn 0.3s;
`;
overlay.innerHTML = `
<div style="
background: #1a1a2e; border: 1px solid #333; border-radius: 12px;
padding: 28px 32px; max-width: 560px; width: 90%; color: #e0e0e0;
font-family: 'Inter', 'Segoe UI', sans-serif; box-shadow: 0 8px 40px rgba(0,0,0,0.6);
max-height: 90vh; overflow-y: auto;
">
<div style="font-size: 20px; font-weight: 700; color: #4caf50; margin-bottom: 4px;">
REXXX.MENU v15.1
</div>
<div style="font-size: 12px; color: #888; margin-bottom: 16px;">Chess.com Cheat Engine - v15.1 account-level hardening</div>
<div style="
background: rgba(255,60,60,0.12); border: 1px solid #ff4444; border-radius: 8px;
padding: 12px 14px; margin-bottom: 14px; font-size: 12px; line-height: 1.55;
">
<div style="font-weight: 700; color: #ff6666; margin-bottom: 4px;">READ THIS BEFORE STARTING</div>
<div style="color: #ffcccc;">
<strong>No script is undetectable.</strong> Chess.com's fair-play system runs offline analysis after games and
looks at engine correlation, timing distribution, click telemetry, and rating curves.
With aggressive use this script gets accounts flagged in days; with conservative use, weeks.
</div>
<div style="color: #ff6666; font-weight: 700; margin-top: 8px;">
ALWAYS use an alt. Never run this on an account you care about.
</div>
</div>
<div style="font-size: 13px; line-height: 1.6; margin-bottom: 12px;">
<strong>New in v15.1 (account-level hardening):</strong>
<ul style="margin: 4px 0 0 16px; padding: 0; color: #cfd8dc;">
<li><strong>Warmup:</strong> new accounts play ~350 ELO below target for ~20 games, ramping up.</li>
<li><strong>Winrate targeting:</strong> script pushes your lifetime winrate toward a target % (default 52%) across sessions.</li>
<li><strong>Hard repertoire:</strong> each account picks 1-2 openings per color and sticks to them.</li>
<li><strong>Tilt after loss:</strong> worse play + slower timing for 2-4 games after a defeat.</li>
<li><strong>Smart premove gating:</strong> only premoves when reply is forced or an obvious recapture.</li>
<li><strong>Time-control lock:</strong> refuses to auto-queue a different TC than the one you started the session in.</li>
<li><strong>Engine rotation:</strong> per-game random engine source (varies tactical signature).</li>
<li><strong>Messy resignation:</strong> hesitates, sometimes blunders first, sometimes holds lost positions.</li>
<li><strong>Hardware persona:</strong> per-account mouse/trackpad/tablet personality affects drag feel.</li>
</ul>
</div>
<div style="font-size: 13px; line-height: 1.6; margin-bottom: 12px;">
<strong>Recommended setup for longer survival:</strong>
<ul style="margin: 4px 0 0 16px; padding: 0; color: #cfd8dc;">
<li>Target rating ≤ 1800 (high ratings invite review faster).</li>
<li>Max 4 games/hr, max 5 games/session.</li>
<li>Leave all v15.1 Account & Behavior toggles ON.</li>
<li>When switching Chess.com accounts, press "Reset Account State" in the SAFETY tab.</li>
<li>Vary play across days. Don't grind 50 games in one sitting.</li>
</ul>
</div>
<div style="font-size: 12px; color: #888; margin-bottom: 16px; line-height: 1.5;">
Press <span style="background:#333;padding:2px 6px;border-radius:3px;font-family:monospace;">A</span> to toggle auto-play
once a game starts. Defaults are tuned for safety — be cautious before raising any caps.
</div>
<div style="text-align: center;">
<button id="ba-welcome-dismiss" style="
background: #4caf50; color: white; border: none; border-radius: 6px;
padding: 10px 32px; font-size: 14px; font-weight: 600; cursor: pointer;
">I Understand — Let's Go</button>
</div>
</div>
`;
document.body.appendChild(overlay);
overlay.querySelector('#ba-welcome-dismiss').addEventListener('click', () => {
overlay.style.opacity = '0';
overlay.style.transition = 'opacity 0.3s';
setTimeout(() => overlay.remove(), 300);
});
},
init: async () => {
UI.injectStyles();
UI.createInterface();
// Load saved settings
Settings.init(CONFIG);
// Fresh page load = fresh session: clear per-session locks/overrides.
// Account-level persistent state (totalGamesPlayed, repertoire, hardware,
// recentResults) stays; only per-session things like sessionTC reset.
if (CONFIG.account) {
CONFIG.account.sessionTC = null;
CONFIG.account.currentEngine = null;
}
// Show first-time welcome modal
Main.showWelcome();
// Apply rating profile
RatingProfile.apply(CONFIG.targetRating);
// Initialize persistent weakness profile
WeaknessProfile.init();
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();
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.target.matches('input, textarea, [contenteditable]')) return;
if (e.key === 'a' && !e.ctrlKey && !e.shiftKey) {
CONFIG.auto.enabled = !CONFIG.auto.enabled;
Utils.log(`Auto-Play: ${CONFIG.auto.enabled}`);
UI.updatePanel(State.currentEval, {});
Settings.save(CONFIG);
}
if (e.key === 'x') {
UI.toggleStealth();
}
if (e.key === 'r' && !e.ctrlKey) {
Settings.reset(CONFIG);
RatingProfile.apply(CONFIG.targetRating);
UI.refreshSliders();
Utils.log('Settings reset to defaults');
}
});
// Clock polling
setInterval(() => Game.readClock(), 2000);
},
detectGameResult: () => {
// Check for win
const winHeader = document.querySelector('.game-over-modal-header-userWon');
if (winHeader) return 'win';
// Check title text as fallback
const titleEl = document.querySelector('[data-cy="header-title-component"]');
if (titleEl) {
const text = titleEl.textContent.trim().toLowerCase();
if (text.includes('you won')) return 'win';
if (text.includes('you lost') || text.includes('checkmate') || text.includes('time')) return 'loss';
if (text.includes('draw') || text.includes('stalemate')) return 'draw';
}
// Check loss
const lossHeader = document.querySelector('.game-over-modal-header-userLost');
if (lossHeader) return 'loss';
// Check draw
const drawHeader = document.querySelector('.game-over-modal-header-draw');
if (drawHeader) return 'draw';
return 'unknown';
},
updateSessionStats: (result) => {
CONFIG.session.gamesPlayed++;
if (result === 'win') {
CONFIG.session.wins++;
CONFIG.session.currentWinStreak++;
} else {
CONFIG.session.currentWinStreak = 0;
}
// Account-level: persistent winrate window + tilt trigger
const code = result === 'win' ? 'W' : (result === 'loss' ? 'L' : (result === 'draw' ? 'D' : null));
if (code) {
Account.recordResult(code);
Account.maybeStartTilt(code);
}
Utils.log(`Session: Game #${CONFIG.session.gamesPlayed}, result=${result}, winStreak=${CONFIG.session.currentWinStreak}, account totalGames=${CONFIG.account.totalGamesPlayed}`, 'info');
Settings.save(CONFIG);
UI.updateStats();
},
showStreakWarning: () => {
// Remove existing warning if any
const existing = document.querySelector('.ba-streak-warning');
if (existing) existing.remove();
const streak = CONFIG.session.currentWinStreak;
const gamesPlayed = CONFIG.session.gamesPlayed;
const winRate = gamesPlayed > 0 ? (CONFIG.session.wins / gamesPlayed) : 0;
let warningLevel = null;
let message = '';
if (streak >= CONFIG.session.maxWinStreak) {
warningLevel = 'critical';
message = CONFIG.autoLose.enabled
? `WIN STREAK: ${streak} in a row! Auto-Lose mode will activate next game to avoid detection.`
: `WIN STREAK: ${streak} in a row! You should LOSE the next game to avoid detection. Streak this high will trigger anti-cheat review.`;
} else if (streak >= CONFIG.session.maxWinStreak - 2) {
warningLevel = 'high';
message = `Win streak: ${streak}. Getting suspicious - consider losing soon.`;
} else if (gamesPlayed >= 5 && winRate > 0.85) {
warningLevel = 'medium';
message = `Win rate ${(winRate * 100).toFixed(0)}% over ${gamesPlayed} games. Consider losing a game to look natural.`;
}
if (!warningLevel) return false;
const colors = {
critical: { bg: 'rgba(255,0,0,0.15)', border: '#ff4444', text: '#ff6666', icon: '\u26A0' },
high: { bg: 'rgba(255,165,0,0.12)', border: '#ff9800', text: '#ffb74d', icon: '\u26A0' },
medium: { bg: 'rgba(255,255,0,0.08)', border: '#fdd835', text: '#fff176', icon: '\u24D8' },
};
const c = colors[warningLevel];
const warning = document.createElement('div');
warning.className = 'ba-streak-warning';
warning.style.cssText = `
position: fixed; top: 10px; left: 50%; transform: translateX(-50%); z-index: 10002;
background: ${c.bg}; border: 1px solid ${c.border}; border-radius: 8px;
padding: 12px 20px; max-width: 500px; text-align: center;
font-family: 'Inter', sans-serif; font-size: 13px; color: ${c.text};
box-shadow: 0 4px 20px rgba(0,0,0,0.5); backdrop-filter: blur(8px);
animation: fadeIn 0.3s;
`;
warning.innerHTML = `
<div style="font-weight:700; margin-bottom:4px;">${c.icon} ANTI-CHEAT WARNING</div>
<div>${message}</div>
<div style="margin-top:8px; font-size:11px; opacity:0.7; cursor:pointer;" onclick="this.parentElement.remove()">[click to dismiss]</div>
`;
document.body.appendChild(warning);
// Auto-dismiss after 15s for non-critical
if (warningLevel !== 'critical') {
setTimeout(() => warning.remove(), 15000);
}
// If auto-lose is enabled, don't pause - let auto-lose handle it next game
return warningLevel === 'critical' && !CONFIG.autoLose.enabled;
},
checkAutoQueue: async () => {
if (!CONFIG.auto.autoQueue || !CONFIG.auto.enabled) return;
if (Main._queueing) return;
// Verify game-over modal is actually visible
const modal = Utils.query(SELECTORS.gameOver);
if (!modal) return;
Main._queueing = true;
// Detect game result and update session stats (only once per game).
// Use a monotonic gameInstanceId so two games ending on coincidentally
// similar FENs (e.g. repetition draws, stalemates with same final position)
// can't share the same tracking key.
const result = Main.detectGameResult();
if (Main._lastGameResultTracked !== Main._gameInstanceId) {
Main._lastGameResultTracked = Main._gameInstanceId;
Main.updateSessionStats(result);
}
// Show warning if streak is suspicious
const shouldPause = Main.showStreakWarning();
if (shouldPause) {
Utils.log('Auto-Queue: PAUSED - Win streak too high, user should lose a game', 'error');
Main._queueing = false;
return;
}
// TC LOCK: refuse to queue games of a different time control than
// the one we started this session with. Real players stick to one TC.
if (!Account.tcLockAllowsQueue()) {
UI.toast('TC Lock', `Different time control detected - skipping auto-queue`, 'warn', 5000);
Main._queueing = false;
return;
}
// ANTI-DETECTION: Game rate limiter (max games per hour).
// We compute the limit BEFORE pushing so we don't accidentally hit
// (cap+1) when the just-finished game is what would push us over.
const now = Date.now();
if (!CONFIG.session.gameTimestamps) CONFIG.session.gameTimestamps = [];
CONFIG.session.gameTimestamps = CONFIG.session.gameTimestamps.filter(t => now - t < 3600000);
if (CONFIG.session.gameTimestamps.length >= CONFIG.antiDetection.maxGamesPerHour) {
const oldest = CONFIG.session.gameTimestamps[0];
const waitMs = 3600000 - (now - oldest) + Utils.randomRange(60000, 180000);
Utils.log(`Rate limiter: ${CONFIG.session.gameTimestamps.length}/h reached, waiting ${Math.round(waitMs / 1000)}s`, 'warn');
UI.toast('Rate limited', `Hour cap hit — pausing ${Math.round(waitMs / 60000)}m`, 'warn', 6000);
await Utils.sleep(waitMs);
// Re-filter after waking up; older entries fall off naturally.
const after = Date.now();
CONFIG.session.gameTimestamps = CONFIG.session.gameTimestamps.filter(t => after - t < 3600000);
}
CONFIG.session.gameTimestamps.push(Date.now());
// Session break check (with jitter)
const jitter = CONFIG.antiDetection.sessionLengthJitter;
const jitteredMax = Math.round(CONFIG.session.maxGamesPerSession * (1 + (Math.random() * 2 - 1) * jitter));
if (CONFIG.session.gamesPlayed >= jitteredMax) {
const breakMs = CONFIG.session.breakDurationMs + Utils.randomRange(30000, 180000);
Utils.log(`Session break: ${Math.round(breakMs / 1000)}s after ${CONFIG.session.gamesPlayed} games (jittered limit: ${jitteredMax})`, 'warn');
await Utils.sleep(breakMs);
CONFIG.session.gamesPlayed = 0;
CONFIG.session.wins = 0;
CONFIG.session.currentWinStreak = 0;
// Reset TC lock so a new session can pick a fresh time control
Account.clearSessionTC();
}
// ANTI-DETECTION: Minimum break between games
const betweenGames = CONFIG.antiDetection.minBreakBetweenGames;
const breakDelay = Utils.humanDelay(betweenGames.min, betweenGames.max);
Utils.log(`Between-game delay: ${Math.round(breakDelay / 1000)}s`, 'debug');
await Utils.sleep(breakDelay);
// Try to click the new game button
const newGameSelectors = [
'button[data-cy="game-over-modal-new-game-button"]',
'.game-over-secondary-actions-row-component 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-modal-shell-buttons button.cc-button-secondary',
'.game-over-buttons-button',
'.game-over-button-component.primary',
'.ui_v5-button-component.ui_v5-button-primary',
];
for (let attempt = 0; attempt < 10; attempt++) {
for (const sel of newGameSelectors) {
const btn = document.querySelector(sel);
if (btn && btn.offsetParent !== null) {
// Prefer "New Game" over "Rematch" if both exist
if (sel.includes('rematch')) {
const newGameBtn = document.querySelector(newGameSelectors[0]);
if (newGameBtn && newGameBtn.offsetParent !== null) continue;
}
const delay = Utils.humanDelay(2000, 5000);
Utils.log(`Auto-Queue: Clicking "${btn.textContent.trim()}" in ${Math.round(delay)}ms (game #${CONFIG.session.gamesPlayed}, streak: ${CONFIG.session.currentWinStreak})...`);
await Utils.sleep(delay);
btn.click();
// Wait and verify the modal closed
await Utils.sleep(1500);
const stillOpen = Utils.query(SELECTORS.gameOver);
if (stillOpen) {
Utils.log('Auto-Queue: Modal still open, trying close button...', 'warn');
const closeBtn = document.querySelector('.game-over-modal-header-close, [data-cy="close-board-modal"], .cc-close-button-component');
if (closeBtn) {
closeBtn.click();
await Utils.sleep(1000);
}
}
Main._queueing = false;
return;
}
}
await Utils.sleep(1500);
}
// Last resort: try closing the modal and looking for new game button elsewhere
Utils.log('Auto-Queue: Standard buttons not found, trying close + page buttons...', 'warn');
const closeBtn = document.querySelector('.game-over-modal-header-close, [data-cy="close-board-modal"], .cc-close-button-component');
if (closeBtn) {
closeBtn.click();
await Utils.sleep(2000);
// Look for any new game button on the page
const fallbackBtn = document.querySelector('button[data-cy="new-game-index-main"], .new-game-button, [class*="new-game"]');
if (fallbackBtn) {
fallbackBtn.click();
}
}
Utils.log('Auto-Queue: Could not find new game button', 'warn');
Main._queueing = false;
},
_loopScheduled: false,
_scheduleLoop: () => {
if (Main._loopScheduled) return;
Main._loopScheduled = true;
setTimeout(() => {
Main._loopScheduled = false;
Main.gameLoop();
}, 50);
},
setupObservers: () => {
const movesList = Utils.query(SELECTORS.moves) || document.body;
const observer = new MutationObserver(() => {
Main._scheduleLoop();
});
observer.observe(movesList, { childList: true, subtree: true, characterData: true });
let gameOverCheckScheduled = false;
const gameOverObserver = new MutationObserver(() => {
if (gameOverCheckScheduled) return;
gameOverCheckScheduled = true;
setTimeout(() => {
gameOverCheckScheduled = false;
if (Utils.query(SELECTORS.gameOver) && CONFIG.auto.enabled && CONFIG.auto.autoQueue) {
Main.checkAutoQueue();
}
}, 500);
});
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();
}
}, 5000);
},
_autoQueueScheduled: false,
_lastLoopRun: 0,
gameLoop: () => {
// Debounce: skip if called again within 100ms (prevents tab-resume storm)
const now = Date.now();
if (now - Main._lastLoopRun < 100) return;
Main._lastLoopRun = now;
// Run background telemetry noise generator
TelemetryNoise.tick();
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)) {
Main._gameInstanceId++;
Utils.log(`New game detected (#${Main._gameInstanceId}) - resetting humanization`, 'warn');
UI.toast('New Game', `Personality reset — GL HF`, 'success', 3000);
HumanStrategy.resetGame();
Main._queueing = false;
}
State.lastFen = fen;
State.currentEval = null;
State.candidates = {};
UI.clearOverlay();
UI.updatePanel(null, null);
// Read clock
Game.readClock();
if (Utils.query(SELECTORS.gameOver)) {
if (CONFIG.auto.autoQueue && !Main._autoQueueScheduled) {
Main._autoQueueScheduled = true;
setTimeout(() => {
Main._autoQueueScheduled = false;
Main.checkAutoQueue();
}, 2000);
}
return;
}
if (Game.isMyTurn(fen)) {
// --- Recapture detection ---
// Did the opponent just capture? Compare piece count in old vs new FEN
if (State.lastFen) {
const oldPieces = (State.lastFen.split(' ')[0].match(/[a-zA-Z]/g) || []).length;
const newPieces = (fen.split(' ')[0].match(/[a-zA-Z]/g) || []).length;
State.lastMoveWasCapture = newPieces < oldPieces;
} else {
State.lastMoveWasCapture = false;
}
UI.updateStatus(UI._styleAccents[CONFIG.playStyle] || '#4caf50');
Engine.analyze(fen);
} else {
// It's opponent's turn — save our current eval for surprise detection
if (State.currentEval) {
State.human.evalBeforeOpponentMove = State.currentEval.value;
}
UI.updateStatus('#888');
}
}
};
Main.init();
})();