Auto-plays chess on chess.com with auto-rematch and proper check handling
// ==UserScript==
// @name Chess.com Auto Bot
// @namespace http://tampermonkey.net/
// @version 4.0
// @description Auto-plays chess on chess.com with auto-rematch and proper check handling
// @author nedia
// @match https://www.chess.com/*
// @match https://chess.com/*
// @run-at document-end
// @require https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.3/chess.min.js
// @license MIT
// @grant none
// ==/UserScript==
(function () {
'use strict';
// ─── Config ────────────────────────────────────────────────────────────────
const MOVE_DELAY_MS = 800;
const CLICK_PAUSE_MS = 300;
const POLL_MS = 1000;
const REMATCH_DELAY = 3000;
const SEARCH_DEPTH = 3;
// ─── Piece values + piece-square tables ───────────────────────────────────
const PIECE_VALUE = { p:100, n:320, b:330, r:500, q:900, k:20000 };
const PST = {
p:[0,0,0,0,0,0,0,0,50,50,50,50,50,50,50,50,10,10,20,30,30,20,10,10,
5,5,10,25,25,10,5,5,0,0,0,20,20,0,0,0,5,-5,-10,0,0,-10,-5,5,
5,10,10,-20,-20,10,10,5,0,0,0,0,0,0,0,0],
n:[-50,-40,-30,-30,-30,-30,-40,-50,-40,-20,0,0,0,0,-20,-40,
-30,0,10,15,15,10,0,-30,-30,5,15,20,20,15,5,-30,
-30,0,15,20,20,15,0,-30,-30,5,10,15,15,10,5,-30,
-40,-20,0,5,5,0,-20,-40,-50,-40,-30,-30,-30,-30,-40,-50],
b:[-20,-10,-10,-10,-10,-10,-10,-20,-10,0,0,0,0,0,0,-10,
-10,0,5,10,10,5,0,-10,-10,5,5,10,10,5,5,-10,
-10,0,10,10,10,10,0,-10,-10,10,10,10,10,10,10,-10,
-10,5,0,0,0,0,5,-10,-20,-10,-10,-10,-10,-10,-10,-20],
r:[0,0,0,0,0,0,0,0,5,10,10,10,10,10,10,5,-5,0,0,0,0,0,0,-5,
-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,
-5,0,0,0,0,0,0,-5,0,0,0,5,5,0,0,0],
q:[-20,-10,-10,-5,-5,-10,-10,-20,-10,0,0,0,0,0,0,-10,
-10,0,5,5,5,5,0,-10,-5,0,5,5,5,5,0,-5,
0,0,5,5,5,5,0,-5,-10,5,5,5,5,5,0,-10,
-10,0,5,0,0,0,0,-10,-20,-10,-10,-5,-5,-10,-10,-20],
k:[-30,-40,-40,-50,-50,-40,-40,-30,-30,-40,-40,-50,-50,-40,-40,-30,
-30,-40,-40,-50,-50,-40,-40,-30,-30,-40,-40,-50,-50,-40,-40,-30,
-20,-30,-30,-40,-40,-30,-30,-20,-10,-20,-20,-20,-20,-20,-20,-10,
20,20,0,0,0,0,20,20,20,30,10,0,0,10,30,20],
};
// ─── State ────────────────────────────────────────────────────────────────
let botActive = false;
let autoRematch = true;
// ─── UI ───────────────────────────────────────────────────────────────────
function buildPanel() {
if (document.getElementById('chess-bot-panel')) return;
const style = document.createElement('style');
style.textContent = `
#chess-bot-panel {
position: fixed !important; bottom: 20px !important; right: 20px !important;
z-index: 2147483647 !important; background: #1e1e1e !important;
color: #f0f0f0 !important; font-family: monospace, monospace !important;
font-size: 13px !important; border-radius: 10px !important;
padding: 14px 16px !important; box-shadow: 0 4px 20px rgba(0,0,0,0.8) !important;
min-width: 215px !important; user-select: none !important; line-height: 1.4 !important;
}
#chess-bot-panel button {
display: block !important; width: 100% !important; margin: 8px 0 !important;
padding: 7px 0 !important; border: none !important; border-radius: 6px !important;
font-size: 13px !important; font-weight: bold !important; cursor: pointer !important;
background: #2e7d32 !important; color: #fff !important;
}
#chess-bot-panel button.on { background: #c62828 !important; }
#chess-bot-panel label {
display: flex !important; align-items: center !important; gap: 6px !important;
font-size: 12px !important; color: #bbb !important; cursor: pointer !important;
}
`;
document.head.appendChild(style);
const panel = document.createElement('div');
panel.id = 'chess-bot-panel';
panel.innerHTML = `
<b style="font-size:14px">♟ Chess Bot</b>
<div id="cbot-status" style="color:#aaa;font-size:12px;margin:6px 0 4px">Idle — click Start</div>
<button id="cbot-btn">▶ Start Bot</button>
<label><input type="checkbox" id="cbot-rematch" checked> Auto-rematch</label>
`;
document.body.appendChild(panel);
document.getElementById('cbot-btn').addEventListener('click', toggleBot);
document.getElementById('cbot-rematch').addEventListener('change', function () {
autoRematch = this.checked;
});
}
function toggleBot() {
botActive = !botActive;
const btn = document.getElementById('cbot-btn');
if (botActive) {
btn.textContent = 'Stop Bot';
btn.classList.add('on');
setStatus('Running...');
runLoop();
} else {
btn.textContent = 'Start Bot';
btn.classList.remove('on');
setStatus('Stopped');
}
}
function setStatus(msg) {
const el = document.getElementById('cbot-status');
if (el) el.textContent = msg;
}
new MutationObserver(() => {
if (!document.getElementById('chess-bot-panel') && document.body) buildPanel();
}).observe(document.documentElement, { childList: true, subtree: true });
// ─── Board helpers ─────────────────────────────────────────────────────────
function getBoardEl() {
return document.querySelector('chess-board') ||
document.querySelector('wc-chess-board') ||
document.querySelector('.board');
}
function isFlipped() {
const b = getBoardEl();
return b ? b.classList.contains('flipped') : false;
}
function getMyColor() { return isFlipped() ? 'b' : 'w'; }
// ─── Read piece positions directly from the DOM ────────────────────────────
// Returns a map of { 'e4': { color: 'w', type: 'p' }, ... }
// Chess.com pieces look like: <div class="piece wp square-52">
// where 'w'/'b' = color, 'p'/'n'/'b'/'r'/'q'/'k' = type,
// and square-XY means file X (1=a … 8=h), rank Y (1–8).
function getPiecesFromDOM() {
const map = {};
document.querySelectorAll('.piece').forEach(el => {
const classes = Array.from(el.classList);
const typeClass = classes.find(c => /^[wb][pnbrqk]$/.test(c));
const squareClass = classes.find(c => /^square-\d{2}$/.test(c));
if (!typeClass || !squareClass) return;
const color = typeClass[0];
const type = typeClass[1];
const fileNum = parseInt(squareClass[7], 10); // 1–8
const rankNum = parseInt(squareClass[8], 10); // 1–8
const sq = String.fromCharCode(96 + fileNum) + rankNum; // e.g. 'e2'
map[sq] = { color, type };
});
return map;
}
// Compare the DOM piece map against chess.js's board.
// Returns true if they match (ignoring castling/ep details).
function positionsMatch(game, domPieces) {
const board = game.board();
// Check every square
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const jsPiece = board[r][f];
const sq = String.fromCharCode(97 + f) + (8 - r);
const domPiece = domPieces[sq];
if (!jsPiece && domPiece) return false;
if (jsPiece && !domPiece) return false;
if (jsPiece && domPiece) {
if (jsPiece.color !== domPiece.color) return false;
if (jsPiece.type !== domPiece.type) return false;
}
}
}
return true;
}
// Build a game from the move list, then verify it matches the DOM.
// If they don't match, try feeding one fewer / more move until they do,
// or fall back to a FEN reconstructed from the DOM.
function buildGame() {
const selectors = [
'.main-line-row .node-highlight-content',
'.main-line-row .move-text-component',
'vertical-move-list .node-highlight-content',
'[data-ply] .node-highlight-content',
'.moves-table .move',
'.move-list .node .move',
];
let texts = [];
for (const sel of selectors) {
const els = document.querySelectorAll(sel);
if (els.length) {
texts = Array.from(els)
// Strip annotations and move numbers like "1." "23."
.map(e => e.textContent.replace(/[+#!?]/g, '').trim())
.filter(t => t && !/^\d+\.+$/.test(t))
.filter(Boolean);
if (texts.length) break;
}
}
// Replay moves into chess.js
const game = new Chess();
for (const san of texts) {
try {
if (!game.move(san, { sloppy: true })) break;
} catch (_) { break; }
}
// ── Verify against DOM ───────────────────────────────────────────────────
const domPieces = getPiecesFromDOM();
if (Object.keys(domPieces).length === 0) {
// DOM not ready yet, return whatever we have
return game;
}
if (positionsMatch(game, domPieces)) {
return game; // All good
}
// chess.js is out of sync. Try trimming moves one-by-one from the end
// (sometimes the move list has an extra half-move we couldn't parse).
for (let trim = 1; trim <= 3; trim++) {
const g2 = new Chess();
const trimmed = texts.slice(0, texts.length - trim);
let ok = true;
for (const san of trimmed) {
try { if (!g2.move(san, { sloppy: true })) { ok = false; break; } }
catch (_) { ok = false; break; }
}
if (ok && positionsMatch(g2, domPieces)) {
console.log('[ChessBot] Synced by trimming', trim, 'move(s)');
return g2;
}
}
// Last resort: build the position from the DOM directly.
// We can't know castling rights or en passant from the DOM alone,
// but at least the piece positions and turn will be correct, which
// is enough to handle check properly.
console.warn('[ChessBot] Falling back to DOM-derived position');
return buildGameFromDOM(domPieces, game.turn());
}
// Construct a Chess() instance from raw DOM piece positions.
// Determines whose turn it is from the active clock highlight.
function buildGameFromDOM(domPieces, fallbackTurn) {
// Try to read whose turn it is from the clock highlights
const turn = getActiveColor() || fallbackTurn;
// Build a FEN string from piece positions
let fen = '';
for (let rank = 8; rank >= 1; rank--) {
let empty = 0;
for (let file = 1; file <= 8; file++) {
const sq = String.fromCharCode(96 + file) + rank;
const p = domPieces[sq];
if (p) {
if (empty) { fen += empty; empty = 0; }
const ch = p.type; // p n b r q k
fen += p.color === 'w' ? ch.toUpperCase() : ch;
} else {
empty++;
}
}
if (empty) fen += empty;
if (rank > 1) fen += '/';
}
// Append turn, and assume full castling rights / no en passant
// (conservative — won't offer castling if rights are actually lost,
// but will never make an illegal move)
fen += ` ${turn} KQkq - 0 1`;
try {
const g = new Chess(fen);
return g;
} catch (_) {
// If even that fails, return an empty game (bot will skip its turn)
return new Chess();
}
}
// Detect whose turn it is by looking for the active clock indicator
function getActiveColor() {
// chess.com highlights the active player's clock
const activeClock = document.querySelector('.clock-component.clock-player-turn');
if (!activeClock) return null;
// The clock is either at the top (opponent) or bottom (us) of the board
// "bottom" = white normally, black when flipped
const allClocks = Array.from(document.querySelectorAll('.clock-component'));
if (allClocks.length < 2) return null;
const lastClock = allClocks[allClocks.length - 1];
const isBottomActive = lastClock.classList.contains('clock-player-turn');
if (isFlipped()) {
return isBottomActive ? 'b' : 'w';
} else {
return isBottomActive ? 'w' : 'b';
}
}
// ─── Evaluation + minimax ─────────────────────────────────────────────────
function evaluate(game) {
if (game.in_checkmate()) return game.turn() === 'w' ? -30000 : 30000;
if (game.in_draw()) return 0;
let score = 0;
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const p = game.board()[r][f];
if (!p) continue;
const idx = r * 8 + f;
const pval = PIECE_VALUE[p.type] || 0;
const pst = (PST[p.type] || [])[p.color === 'w' ? idx : 63 - idx] || 0;
score += p.color === 'w' ? pval + pst : -(pval + pst);
}
}
return score;
}
function minimax(game, depth, alpha, beta, max) {
if (depth === 0 || game.game_over()) return evaluate(game);
const moves = game.moves();
let best = max ? -Infinity : Infinity;
for (const m of moves) {
game.move(m);
const val = minimax(game, depth - 1, alpha, beta, !max);
game.undo();
if (max) { if (val > best) best = val; alpha = Math.max(alpha, val); }
else { if (val < best) best = val; beta = Math.min(beta, val); }
if (beta <= alpha) break;
}
return best;
}
function getBestMove(game) {
// game.moves() in chess.js ONLY returns legal moves —
// if in check, it only returns moves that escape check.
const moves = game.moves({ verbose: true });
if (!moves.length) return null;
const max = game.turn() === 'w';
let best = moves[0], bestVal = max ? -Infinity : Infinity;
for (const mv of moves) {
game.move(mv);
const val = minimax(game, SEARCH_DEPTH - 1, -Infinity, Infinity, !max);
game.undo();
if (max ? val > bestVal : val < bestVal) { bestVal = val; best = mv; }
}
return best;
}
// ─── Click execution ───────────────────────────────────────────────────────
function fireAt(x, y) {
const el = document.elementFromPoint(x, y);
if (!el) return;
const opts = { bubbles:true, cancelable:true, clientX:x, clientY:y, screenX:x, screenY:y, button:0, buttons:1 };
const pOpts = { ...opts, pointerId:1, isPrimary:true, pointerType:'mouse' };
el.dispatchEvent(new PointerEvent('pointerover', pOpts));
el.dispatchEvent(new PointerEvent('pointerenter', { ...pOpts, bubbles:false }));
el.dispatchEvent(new MouseEvent ('mouseover', opts));
el.dispatchEvent(new PointerEvent('pointermove', pOpts));
el.dispatchEvent(new MouseEvent ('mousemove', opts));
el.dispatchEvent(new PointerEvent('pointerdown', pOpts));
el.dispatchEvent(new MouseEvent ('mousedown', opts));
el.dispatchEvent(new PointerEvent('pointerup', pOpts));
el.dispatchEvent(new MouseEvent ('mouseup', opts));
el.dispatchEvent(new MouseEvent ('click', opts));
}
function squareCoords(sq) {
const board = getBoardEl();
if (!board) return null;
const rect = board.getBoundingClientRect();
const sqSize = rect.width / 8;
const file = sq.charCodeAt(0) - 97;
const rank = parseInt(sq[1]) - 1;
const flip = isFlipped();
return {
x: rect.left + (flip ? (7 - file + 0.5) : (file + 0.5)) * sqSize,
y: rect.top + (flip ? (rank + 0.5) : (7 - rank + 0.5)) * sqSize,
};
}
function clickSource(sq) {
const fileNum = sq.charCodeAt(0) - 96;
const rankNum = parseInt(sq[1]);
const pieceEl = document.querySelector(`.piece.square-${fileNum}${rankNum}`);
if (pieceEl) {
const r = pieceEl.getBoundingClientRect();
fireAt(r.left + r.width / 2, r.top + r.height / 2);
return;
}
const c = squareCoords(sq);
if (c) fireAt(c.x, c.y);
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function executeMove(mv) {
clickSource(mv.from);
await sleep(CLICK_PAUSE_MS);
const dest = squareCoords(mv.to);
if (dest) fireAt(dest.x, dest.y);
if (mv.promotion) {
await sleep(500);
const q = document.querySelector('.promotion-piece.wq,.promotion-piece.bq,[data-piece="q"]');
if (q) q.click();
}
}
// ─── Auto-rematch ─────────────────────────────────────────────────────────
function tryRematch() {
const keywords = ['rematch', 'new game', 'play again', 'new opponent'];
const sels = [
'[data-cy="new-game-index-btn"]', '[data-cy="rematch-button"]',
'button[class*="rematch"]', '.game-over-modal-content button',
'.modal-game-over-component button', '.game-over-buttons-component button',
'button.cc-button-component',
];
for (const sel of sels) {
for (const btn of document.querySelectorAll(sel)) {
if (keywords.some(k => btn.textContent.toLowerCase().includes(k))) {
btn.click(); return true;
}
}
}
return false;
}
// ─── Main loop ─────────────────────────────────────────────────────────────
async function runLoop() {
while (botActive) {
await sleep(POLL_MS);
if (!botActive) break;
try {
const game = buildGame();
const myCol = getMyColor();
if (game.game_over()) {
const why = game.in_checkmate() ? 'Checkmate'
: game.in_stalemate() ? 'Stalemate' : 'Game over';
setStatus(why);
if (autoRematch) { await sleep(REMATCH_DELAY); tryRematch(); }
continue;
}
if (game.turn() !== myCol) {
setStatus(game.in_check() ? 'Opponent in check...' : "Opponent's turn...");
continue;
}
if (game.in_check()) setStatus('In check! Finding escape...');
else setStatus('Thinking...');
await sleep(MOVE_DELAY_MS);
const mv = getBestMove(game);
if (!mv) { setStatus('No legal moves'); continue; }
setStatus('Playing ' + mv.san);
await executeMove(mv);
} catch (err) {
setStatus('Error: ' + err.message);
console.error('[ChessBot]', err);
}
}
}
// ─── Boot ─────────────────────────────────────────────────────────────────
function init() { buildPanel(); }
init();
setTimeout(init, 1000);
setTimeout(init, 3000);
setTimeout(init, 6000);
})();