Chess.com Real-Time AI Analysis with Player Color Indicator

실시간으로 FEN을 업로드하고 AI 분석을 받아오며, 마지막 수와 총 수(전체 수), 그리고 탭 플레이어의 색상을 표시하고 업데이트 시 무지개 색 표시기를 변경합니다.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         Chess.com Real-Time AI Analysis with Player Color Indicator
// @namespace    http://tampermonkey.net/
// @version      2.9
// @description  실시간으로 FEN을 업로드하고 AI 분석을 받아오며, 마지막 수와 총 수(전체 수), 그리고 탭 플레이어의 색상을 표시하고 업데이트 시 무지개 색 표시기를 변경합니다.
// @author       You
// @match        *://www.chess.com/play/computer*
// @grant        GM_xmlhttpRequest
// @connect      lichess.org
// ==/UserScript==

(function() {
    'use strict';

    const rainbowColors = ['#FF0000','#FF7F00','#FFFF00','#00FF00','#0000FF','#4B0082','#8F00FF'];
    let colorIndex = 0;
    let lastFen = '';
    let lastFullMoveCount = 0;
    let lastOutput = '';

    // UI 요소 생성
    const infoBox = document.createElement('div');
    Object.assign(infoBox.style, {
        position: 'fixed', top: '10px', right: '10px',
        padding: '8px 12px', background: 'rgba(0,0,0,0.7)', color: '#fff',
        fontFamily: 'monospace', fontSize: '14px', zIndex: 9999,
        borderRadius: '4px', whiteSpace: 'pre'
    });
    const textNode = document.createTextNode('분석 대기 중...');
    const indicator = document.createElement('div');
    Object.assign(indicator.style, {
        width: '12px', height: '12px', marginTop: '6px', borderRadius: '50%',
        backgroundColor: rainbowColors[0]
    });
    infoBox.append(textNode, document.createElement('br'), indicator);
    document.body.append(infoBox);

    // 보드 말 배치에서 FEN 위치 필드만 추출
    function extractPosition() {
        const board = Array.from({ length: 8 }, () => Array(8).fill(''));
        document.querySelectorAll('.piece').forEach(el => {
            const cls = el.className;
            const sqMatch = cls.match(/square-(\d\d)/);
            if (!sqMatch) return;
            const sq = sqMatch[1];
            const file = parseInt(sq.charAt(0), 10) - 1;
            const rank = parseInt(sq.charAt(1), 10) - 1;
            if (isNaN(file) || isNaN(rank)) return;
            const pCharMatch = cls.match(/piece [wb]([prnbqk])/);
            if (!pCharMatch) return;
            const map = { p:'p', r:'r', n:'n', b:'b', q:'q', k:'k' };
            let p = map[pCharMatch[1]];
            if (cls.includes(' wp ')) p = p.toUpperCase();
            board[7 - rank][file] = p;
        });
        return board.map(row => {
            let empty = 0, str = '';
            row.forEach(cell => {
                if (!cell) empty++; else { if (empty) { str += empty; empty = 0; } str += cell; }
            });
            return str + (empty ? empty : '');
        }).join('/');
    }

    // 수 정보(전체 수, half moves, 마지막 수) 가져오기
    function getMovesInfo() {
        const rows = document.querySelectorAll('.main-line-row.move-list-row');
        let fullCount = 0;
        if (rows.length) {
            fullCount = parseInt(rows[rows.length - 1].getAttribute('data-whole-move-number'), 10) || 0;
        }
        const ply = document.querySelectorAll('.node.main-line-ply');
        const halfCount = ply.length;
        const lastMove = halfCount ? ply[halfCount - 1].textContent.trim() : '';
        return { fullCount, halfCount, lastMove };
    }

    // 전체 FEN 생성
    function makeFEN(pos, turn) {
        return `${pos} ${turn} KQkq - 0 1`;
    }

    // indicator 색상 순환
    function rotateIndicator() {
        colorIndex = (colorIndex + 1) % rainbowColors.length;
        indicator.style.backgroundColor = rainbowColors[colorIndex];
    }

    // AI 분석 요청 및 화면 업데이트
    function updateAnalysis(fen, myColor, lastMove, fullCount) {
        GM_xmlhttpRequest({
            method: 'GET',
            url: 'https://lichess.org/api/cloud-eval?fen=' + encodeURIComponent(fen) + '&multiPv=1',
            headers: { 'Accept': 'application/json' },
            onload: res => {
                try {
                    const data = JSON.parse(res.responseText);
                    let out = `총 수: ${fullCount}\n내 색상: ${myColor}\n마지막 수: ${lastMove || '-'}\n`;
                    if (data.pvs && data.pvs.length > 0) {
                        const pv = data.pvs[0];
                        const cp = pv.cp, mate = pv.mate;
                        const move = pv.moves.split(' ')[0];
                        const human = move.slice(0,2) + '→' + move.slice(2,4);
                        const ev = mate != null ? `M${mate}` : (cp != null ? (cp/100).toFixed(2) : '?');
                        out += `평가: ${ev}\n추천: ${human}`;
                    } else {
                        out += `분석 정보 없음`;
                    }
                    if (out !== lastOutput) {
                        lastOutput = out;
                        textNode.nodeValue = out;
                        rotateIndicator();
                    }
                } catch (e) {
                    console.error('AI 분석 오류:', e);
                }
            },
            onerror: err => console.error('AI 분석 요청 실패:', err)
        });
    }

    // 메인 루프: 0.1초마다 실행
    setInterval(() => {
        rotateIndicator();
        const pos = extractPosition();
        const { fullCount, halfCount, lastMove } = getMovesInfo();

        // 탭 플레이어 색상 감지 (보드 뒤집힘 상태로 판단)
        const boardEl = document.getElementById('board-play-computer');
        const myColor = boardEl.classList.contains('flipped') ? '흑' : '백';

        const turn = (halfCount % 2 === 0) ? 'w' : 'b';
        const fen = makeFEN(pos, turn);
        if (fen !== lastFen || fullCount !== lastFullMoveCount) {
            lastFen = fen;
            lastFullMoveCount = fullCount;
            updateAnalysis(fen, myColor, lastMove, fullCount);
        }
    }, 100);
})();