Greasy Fork is available in English.

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

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

  1. // ==UserScript==
  2. // @name Chess.com Real-Time AI Analysis with Player Color Indicator
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.9
  5. // @description 실시간으로 FEN을 업로드하고 AI 분석을 받아오며, 마지막 수와 총 수(전체 수), 그리고 탭 플레이어의 색상을 표시하고 업데이트 시 무지개 색 표시기를 변경합니다.
  6. // @author You
  7. // @match *://www.chess.com/play/computer*
  8. // @grant GM_xmlhttpRequest
  9. // @connect lichess.org
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. const rainbowColors = ['#FF0000','#FF7F00','#FFFF00','#00FF00','#0000FF','#4B0082','#8F00FF'];
  16. let colorIndex = 0;
  17. let lastFen = '';
  18. let lastFullMoveCount = 0;
  19. let lastOutput = '';
  20.  
  21. // UI 요소 생성
  22. const infoBox = document.createElement('div');
  23. Object.assign(infoBox.style, {
  24. position: 'fixed', top: '10px', right: '10px',
  25. padding: '8px 12px', background: 'rgba(0,0,0,0.7)', color: '#fff',
  26. fontFamily: 'monospace', fontSize: '14px', zIndex: 9999,
  27. borderRadius: '4px', whiteSpace: 'pre'
  28. });
  29. const textNode = document.createTextNode('분석 대기 중...');
  30. const indicator = document.createElement('div');
  31. Object.assign(indicator.style, {
  32. width: '12px', height: '12px', marginTop: '6px', borderRadius: '50%',
  33. backgroundColor: rainbowColors[0]
  34. });
  35. infoBox.append(textNode, document.createElement('br'), indicator);
  36. document.body.append(infoBox);
  37.  
  38. // 보드 말 배치에서 FEN 위치 필드만 추출
  39. function extractPosition() {
  40. const board = Array.from({ length: 8 }, () => Array(8).fill(''));
  41. document.querySelectorAll('.piece').forEach(el => {
  42. const cls = el.className;
  43. const sqMatch = cls.match(/square-(\d\d)/);
  44. if (!sqMatch) return;
  45. const sq = sqMatch[1];
  46. const file = parseInt(sq.charAt(0), 10) - 1;
  47. const rank = parseInt(sq.charAt(1), 10) - 1;
  48. if (isNaN(file) || isNaN(rank)) return;
  49. const pCharMatch = cls.match(/piece [wb]([prnbqk])/);
  50. if (!pCharMatch) return;
  51. const map = { p:'p', r:'r', n:'n', b:'b', q:'q', k:'k' };
  52. let p = map[pCharMatch[1]];
  53. if (cls.includes(' wp ')) p = p.toUpperCase();
  54. board[7 - rank][file] = p;
  55. });
  56. return board.map(row => {
  57. let empty = 0, str = '';
  58. row.forEach(cell => {
  59. if (!cell) empty++; else { if (empty) { str += empty; empty = 0; } str += cell; }
  60. });
  61. return str + (empty ? empty : '');
  62. }).join('/');
  63. }
  64.  
  65. // 수 정보(전체 수, half moves, 마지막 수) 가져오기
  66. function getMovesInfo() {
  67. const rows = document.querySelectorAll('.main-line-row.move-list-row');
  68. let fullCount = 0;
  69. if (rows.length) {
  70. fullCount = parseInt(rows[rows.length - 1].getAttribute('data-whole-move-number'), 10) || 0;
  71. }
  72. const ply = document.querySelectorAll('.node.main-line-ply');
  73. const halfCount = ply.length;
  74. const lastMove = halfCount ? ply[halfCount - 1].textContent.trim() : '';
  75. return { fullCount, halfCount, lastMove };
  76. }
  77.  
  78. // 전체 FEN 생성
  79. function makeFEN(pos, turn) {
  80. return `${pos} ${turn} KQkq - 0 1`;
  81. }
  82.  
  83. // indicator 색상 순환
  84. function rotateIndicator() {
  85. colorIndex = (colorIndex + 1) % rainbowColors.length;
  86. indicator.style.backgroundColor = rainbowColors[colorIndex];
  87. }
  88.  
  89. // AI 분석 요청 및 화면 업데이트
  90. function updateAnalysis(fen, myColor, lastMove, fullCount) {
  91. GM_xmlhttpRequest({
  92. method: 'GET',
  93. url: 'https://lichess.org/api/cloud-eval?fen=' + encodeURIComponent(fen) + '&multiPv=1',
  94. headers: { 'Accept': 'application/json' },
  95. onload: res => {
  96. try {
  97. const data = JSON.parse(res.responseText);
  98. let out = `총 수: ${fullCount}\n 색상: ${myColor}\n마지막 수: ${lastMove || '-'}\n`;
  99. if (data.pvs && data.pvs.length > 0) {
  100. const pv = data.pvs[0];
  101. const cp = pv.cp, mate = pv.mate;
  102. const move = pv.moves.split(' ')[0];
  103. const human = move.slice(0,2) + '→' + move.slice(2,4);
  104. const ev = mate != null ? `M${mate}` : (cp != null ? (cp/100).toFixed(2) : '?');
  105. out += `평가: ${ev}\n추천: ${human}`;
  106. } else {
  107. out += `분석 정보 없음`;
  108. }
  109. if (out !== lastOutput) {
  110. lastOutput = out;
  111. textNode.nodeValue = out;
  112. rotateIndicator();
  113. }
  114. } catch (e) {
  115. console.error('AI 분석 오류:', e);
  116. }
  117. },
  118. onerror: err => console.error('AI 분석 요청 실패:', err)
  119. });
  120. }
  121.  
  122. // 메인 루프: 0.1초마다 실행
  123. setInterval(() => {
  124. rotateIndicator();
  125. const pos = extractPosition();
  126. const { fullCount, halfCount, lastMove } = getMovesInfo();
  127.  
  128. // 탭 플레이어 색상 감지 (보드 뒤집힘 상태로 판단)
  129. const boardEl = document.getElementById('board-play-computer');
  130. const myColor = boardEl.classList.contains('flipped') ? '흑' : '백';
  131.  
  132. const turn = (halfCount % 2 === 0) ? 'w' : 'b';
  133. const fen = makeFEN(pos, turn);
  134. if (fen !== lastFen || fullCount !== lastFullMoveCount) {
  135. lastFen = fen;
  136. lastFullMoveCount = fullCount;
  137. updateAnalysis(fen, myColor, lastMove, fullCount);
  138. }
  139. }, 100);
  140. })();