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

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

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