Chess.com Eval Display Overlay (Current + Best Next)

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