Chess.com Auto Bot

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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);

})();