Chess.com Auto Bot

Auto-plays chess on chess.com with auto-rematch and proper check handling

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==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">&#9823; 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">&#9654; 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);

})();