Instantly overlays a clean badge showing ONLY the current evaluation (from bar) and best next move evaluation (from analysis lines) in the same top-right location where the old rating used to appear
Size
9.2 KB
Version
1.1
Created
Feb 20, 2026
Updated
17 days ago
1// ==UserScript==
2// @name Chess.com Eval Display Overlay (Current + Best Next)
3// @description Instantly overlays a clean badge showing ONLY the current evaluation (from bar) and best next move evaluation (from analysis lines) in the same top-right location where the old rating used to appear
4// @version 1.1
5// @match https://*.chess.com/*
6// @icon https://www.chess.com/bundles/web/favicons/favicon.46041f2d.ico
7// @grant none
8// ==/UserScript==
9
10(function () {
11 'use strict';
12
13 let badge = null;
14 let dreLabel = null;
15 let diagBox = null;
16 let observer = null;
17
18 function log(...args) {
19 console.log('[Chess Eval Overlay]', ...args);
20 }
21
22 function debounce(func, wait) {
23 let timeout;
24 return function executedFunction(...args) {
25 clearTimeout(timeout);
26 timeout = setTimeout(() => func(...args), wait);
27 };
28 }
29
30 // =========================================================================
31 // UI CREATION
32 // =========================================================================
33 function createUIElements() {
34 log('Creating UI elements');
35
36 // Badge - exactly same position/style as before, just different content
37 badge = document.createElement('div');
38 badge.id = 'chess-eval-overlay-badge';
39 badge.style.cssText = `
40 position: fixed;
41 top: 20px;
42 right: 20px;
43 padding: 12px 20px;
44 border-radius: 8px;
45 background: rgba(0, 0, 0, 0.88);
46 color: #fff;
47 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
48 font-size: 15px;
49 font-weight: 600;
50 z-index: 10000;
51 display: none;
52 box-shadow: 0 4px 12px rgba(0,0,0,0.4);
53 line-height: 1.35;
54 min-width: 170px;
55 text-align: center;
56 cursor: pointer;
57 transition: all 0.2s ease;
58 `;
59 badge.addEventListener('click', () => { badge.style.display = 'none'; });
60
61 // DRE watermark
62 dreLabel = document.createElement('div');
63 dreLabel.id = 'chess-dre-label';
64 dreLabel.textContent = 'DRE';
65 dreLabel.style.cssText = `
66 position: fixed;
67 bottom: 10px;
68 right: 10px;
69 padding: 3px 7px;
70 background: rgba(0,0,0,0.6);
71 color: #fff;
72 font-family: monospace;
73 font-size: 10px;
74 border-radius: 4px;
75 z-index: 9999;
76 cursor: pointer;
77 `;
78 dreLabel.addEventListener('click', toggleDiagBox);
79
80 // Diagnostic box (press E key)
81 diagBox = document.createElement('div');
82 diagBox.id = 'chess-diag-box';
83 diagBox.style.cssText = `
84 position: fixed;
85 bottom: 40px;
86 right: 10px;
87 width: 360px;
88 max-height: 420px;
89 overflow-y: auto;
90 background: rgba(0,0,0,0.92);
91 color: #0f0;
92 font-family: 'Courier New', monospace;
93 font-size: 11.5px;
94 padding: 12px;
95 border-radius: 6px;
96 z-index: 9998;
97 display: none;
98 box-shadow: 0 4px 16px rgba(0,0,0,0.5);
99 `;
100 diagBox.innerHTML = '<div style="color:#fff;font-weight:bold;margin-bottom:8px;">π Eval Overlay Diagnostic</div>';
101
102 const appendElements = () => {
103 if (document.body) {
104 document.body.appendChild(badge);
105 document.body.appendChild(dreLabel);
106 document.body.appendChild(diagBox);
107 log('UI elements appended');
108 } else {
109 setTimeout(appendElements, 80);
110 }
111 };
112 appendElements();
113 }
114
115 function toggleDiagBox() {
116 if (diagBox) {
117 diagBox.style.display = diagBox.style.display === 'none' ? 'block' : 'none';
118 }
119 }
120
121 function updateBadge(currentEval, bestEval) {
122 if (!badge) return;
123
124 const curr = currentEval !== null ? formatEval(currentEval) : 'β';
125 const best = bestEval !== null ? formatEval(bestEval) : 'β';
126
127 badge.innerHTML = `
128 <div style="font-size:13px;opacity:0.75;margin-bottom:2px;">Current eval</div>
129 <div style="font-size:19px;font-weight:700;color:#60a5fa;">${curr}</div>
130 <div style="margin:4px 0;opacity:0.5;font-size:13px;">β</div>
131 <div style="font-size:13px;opacity:0.75;margin-bottom:2px;">Best next</div>
132 <div style="font-size:19px;font-weight:700;color:#4ade80;">${best}</div>
133 `;
134 badge.style.display = 'block';
135 }
136
137 function formatEval(val) {
138 if (Math.abs(val) >= 50) return val > 0 ? 'M+' : 'Mβ';
139 return val >= 0 ? `+${val.toFixed(2)}` : val.toFixed(2);
140 }
141
142 function hideBadge() {
143 if (badge) badge.style.display = 'none';
144 }
145
146 // =========================================================================
147 // DATA EXTRACTION - EXACT SELECTORS YOU PROVIDED
148 // =========================================================================
149 function getCurrentEvaluation() {
150 // Exact selector from your request
151 let el = document.querySelector('#board-layout-evaluation > wc-evaluation-bar > div');
152
153 // Fallbacks for robustness (chess.com sometimes changes class names)
154 if (!el) el = document.querySelector('.evaluation-bar-scoreAbbreviated, .evaluation-bar-score, [class*="evaluation-bar"] .score');
155
156 if (el) {
157 const text = el.textContent.trim();
158 return parseEvaluation(text);
159 }
160 return null;
161 }
162
163 function getBestNextEvaluation() {
164 // Exact selector from your request
165 let container = document.querySelector('#board-layout-sidebar > div.sidebar-content > wc-evaluation-lines');
166
167 // Fallbacks
168 if (!container) container = document.querySelector('wc-evaluation-lines, .engine-lines, [class*="evaluation-lines"]');
169
170 if (container) {
171 // Try to find the top evaluation value inside the web component
172 let evalEl = container.querySelector('.evaluation, [class*="eval"], .score, span[class*="cp"], .cp');
173 if (evalEl) {
174 return parseEvaluation(evalEl.textContent.trim());
175 }
176
177 // Last resort: scan entire text for first valid eval number
178 const allText = container.textContent;
179 const match = allText.match(/[+-]?\d+\.\d{1,2}/);
180 if (match) return parseFloat(match[0]);
181 }
182 return null;
183 }
184
185 function parseEvaluation(text) {
186 if (!text || text === '') return null;
187 text = text.trim().replace(/[^M0-9+\-.\s]/g, '');
188
189 if (text.startsWith('M') || text.startsWith('m')) {
190 const mate = parseInt(text.replace(/[^0-9-]/g, ''));
191 return isNaN(mate) ? null : (text.includes('-') ? -100 : 100);
192 }
193
194 const num = parseFloat(text);
195 return isNaN(num) ? null : num;
196 }
197
198 // =========================================================================
199 // OBSERVER & MAIN LOGIC
200 // =========================================================================
201 function handleAnalysisChange() {
202 // Only run when evaluation elements exist
203 if (!document.querySelector('#board-layout-evaluation')) {
204 hideBadge();
205 return;
206 }
207
208 const current = getCurrentEvaluation();
209 const best = getBestNextEvaluation();
210
211 if (current !== null || best !== null) {
212 updateBadge(current, best);
213
214 // Optional live logging to diag box when open
215 if (diagBox && diagBox.style.display === 'block') {
216 const time = new Date().toLocaleTimeString([], {hour:'2-digit', minute:'2-digit', second:'2-digit'});
217 diagBox.innerHTML += `<div style="border-top:1px solid #333;padding-top:6px;">[${time}] Current: ${current}βββBest: ${best}</div>`;
218 diagBox.scrollTop = diagBox.scrollHeight;
219 }
220 }
221 }
222
223 function setupMutationObserver() {
224 log('Starting MutationObserver');
225 const debouncedHandler = debounce(handleAnalysisChange, 160);
226
227 observer = new MutationObserver(debouncedHandler);
228 observer.observe(document.body, {
229 childList: true,
230 subtree: true,
231 characterData: true,
232 attributes: true,
233 attributeFilter: ['class', 'style']
234 });
235 }
236
237 function main() {
238 log('Chess.com Eval Display Overlay v1.1 started');
239 createUIElements();
240
241 setTimeout(() => {
242 setupMutationObserver();
243 handleAnalysisChange(); // first run
244 }, 900);
245
246 // Keyboard shortcut: press E to toggle diagnostic box
247 document.addEventListener('keydown', (e) => {
248 if (e.key.toLowerCase() === 'e' &&
249 !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) {
250 toggleDiagBox();
251 }
252 });
253 }
254
255 if (document.readyState === 'loading') {
256 document.addEventListener('DOMContentLoaded', main);
257 } else {
258 main();
259 }
260})();