├── images ├── .gitignore ├── table.PNG ├── goodmove.PNG ├── opening.PNG └── warning-tampermonkey.PNG ├── README.md └── script.js /images/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /images/table.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomsihap/lichess-good-moves/HEAD/images/table.PNG -------------------------------------------------------------------------------- /images/goodmove.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomsihap/lichess-good-moves/HEAD/images/goodmove.PNG -------------------------------------------------------------------------------- /images/opening.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomsihap/lichess-good-moves/HEAD/images/opening.PNG -------------------------------------------------------------------------------- /images/warning-tampermonkey.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomsihap/lichess-good-moves/HEAD/images/warning-tampermonkey.PNG -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lichess-good-moves 2 | > Show good moves and book moves into Lichess analysis, the same way Chess.com does. 3 | > The accuracy may be debated and just like chess.com does, there is no consensus on what makes a move being a good move. This is purely informational and to have a more optimisic analysis of you game ! 4 | > 5 | > Please feel free to contribute ! 6 | 7 | 8 | ## Openings 9 | 10 | The script can show you when you and your opponent followed book moves, and which opening have been followed. This is an addition to the already existing opening book in Lichess if you want to see the openings in the PGN. 11 | 12 |  13 | 14 | ## Good moves, excellent moves, brillancies 15 | 16 | You now can see when you have made a good, excellent move or a brillancy ! 17 | 18 |  19 | 20 | They are defined as such : 21 | - Good move !? 22 | - Excellent move ! 23 | - Brillancy !! 24 | 25 | You can also see a summary of your good moves in the analysis table : 26 | 27 |  28 | 29 | 30 | ### How are defined good/excellent/brillancies moves ? 31 | 32 | Since you cannot really tell when a move is good (see [this StackOverflow thread](https://chess.stackexchange.com/questions/24378/why-does-lichess-only-tell-me-my-inaccuracies-mistakes-and-blunders-and)), the decision have been made that moves between the following thresholds are defined as good/excellent/brillancy : 33 | 34 | ``` 35 | From white prospective: 36 | - Good :+0.6 centipawn 37 | - Excellent: +1.0 centipawn 38 | - Brillancy: +2.0 centipawn 39 | ``` 40 | 41 | Also, checkmates in X moves have been defined as `+100` centipawn. 42 | 43 | These values are from my own trial and errors, please feel free to contribute to make these values more accurate. 44 | 45 | 46 | # How to install 47 | 48 | First, you will need to install a browser extension to run external scripts. 49 | 50 | - Chrome: https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=fr 51 | - Firefox: https://addons.mozilla.org/fr/firefox/addon/greasemonkey/ 52 | 53 | When installed, you can get the `script.js` [file here](script.js) and copy-paste it into Tampermonkey/Greasemonkey. 54 | 55 | # How to use 56 | 57 | - Make sure the script is activated and go to the analysis page of any of your games. 58 | - If the analysis haven't been run, you have to start it and refresh the page. 59 | - You WILL be prompted something scary like this : 60 | 61 |  62 | 63 | - Don't worry, you can accept it, this is because the script needs to call an external file (XHR request) [to this file](https://raw.githubusercontent.com/tomsihap/eco.json/master/eco.json) which is a list of ECO codes, variations and names to show the opening name in Lichess. 64 | 65 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Lichess Good Moves 3 | // @namespace https://github.com/tomsihap/lichess-good-moves/ 4 | // @version 0.1 5 | // @description Show Brillant, Best, Excellent moves and book moves as chess.com does. 6 | // @author Thomas Sihapanya 7 | // @require https://greasyfork.org/scripts/47911-font-awesome-all-js/code/Font-awesome%20AllJs.js?version=275337 8 | // @include /^https\:\/\/lichess\.org\/[a-zA-Z0-9]{8,}/ 9 | // @grant GM.xmlHttpRequest 10 | // @grant unsafeWindow 11 | // ==/UserScript== 12 | 13 | (function() { 14 | 'use strict'; 15 | 16 | const GOOD_MOVE_THRESOLD = 0.6; 17 | const EXCELLENT_MOVE_THRESOLD = 1; 18 | const BRILLANT_MOVE_THRESOLD = 2; 19 | const CHECKMATE_IN_X_MOVES_VALUE = 100; 20 | 21 | let goodMoves = { 22 | white : { 23 | 'book' : 0, 24 | 'good' : 0, 25 | 'excellent': 0, 26 | 'brillant': 0, 27 | }, 28 | black : { 29 | 'book' : 0, 30 | 'good' : 0, 31 | 'excellent': 0, 32 | 'brillant': 0, 33 | } 34 | } 35 | 36 | 37 | /** 38 | * Wait for the page to load all its elements before running the script 39 | */ 40 | window.addEventListener('load', function() { 41 | 42 | /** 43 | * Create click event since .click on elements doesn't work quite well in Chrome 44 | */ 45 | let clickEvent = document.createEvent('MouseEvents'); 46 | clickEvent.initMouseEvent('mousedown', true, true); 47 | 48 | /** 49 | * Moves explorer must be opened to detect book moves. If not, user is prompted to run an analysis and reload the page. 50 | */ 51 | const formAnalysis = document.getElementsByClassName('future-game-analysis')[0]; 52 | 53 | if (typeof(formAnalysis) !== 'undefined') { 54 | alert('Lichess Good Moves: please run an analysis and reload the page when finished. You will then be prompted to accept cross-origin resource : you can safely allow it (it will fetch data for an opening API) !'); 55 | } 56 | 57 | /** 58 | * Check if users is in analysis page and analysis has been run 59 | */ 60 | if (document.getElementsByClassName('analyse__tools').length > 0 61 | && document.getElementsByClassName('future-game-analysis').length === 0 62 | && document.getElementsByClassName('computer-analysis active').length > 0) { 63 | 64 | const ecoCodesApiUrl = 'https://raw.githubusercontent.com/tomsihap/eco.json/master/eco.json'; 65 | 66 | /** 67 | * Loads ECO codes API 68 | * https://raw.githubusercontent.com/tomsihap/eco.json/master/eco.json 69 | */ 70 | function loadEcoCodesApi() { 71 | GM.xmlHttpRequest({ 72 | method: "GET", 73 | url:ecoCodesApiUrl, 74 | onload: function(response) { 75 | lichessGoodMoves(JSON.parse(response.responseText)); 76 | }, 77 | onerror: function(err) { 78 | alert('Lichess Good Moves script cannot be launched (maybe you have forbid the access to a cross-origin resource ?) - Refresh the page if you want to start again.'); 79 | } 80 | }); 81 | } 82 | 83 | function loadMoves(ecoCodes) { 84 | let domMoves = document.getElementsByTagName('move'); 85 | let moves = []; 86 | let previousEval = { 87 | value: '+0.0', 88 | symbol: '+', 89 | absVal: '0.0' 90 | }; 91 | 92 | Object.values(domMoves).forEach(domMove => { 93 | if (!domMove.classList.contains('empty')) { 94 | if ("undefined" !== typeof(domMove.childNodes)) { 95 | 96 | domMove.childNodes.forEach(node => { 97 | 98 | if ('SAN' === node.tagName) { 99 | /** 100 | * Handle opening 101 | */ 102 | moves.push(node.innerHTML); 103 | 104 | let currentColor = checkColor(moves.length-1); 105 | let currentPgn = createPgnMoves(moves); 106 | let foundOpening = ecoCodes.find(eco => eco.moves.toLowerCase().trim() == currentPgn.toLowerCase().trim()); 107 | 108 | if (typeof(foundOpening) !== 'undefined') { 109 | handleOpeningMove(node, foundOpening, currentColor); 110 | } 111 | 112 | /** 113 | * Handle evaluation 114 | */ 115 | let currentEval = { 116 | textValue: node.parentElement.getElementsByTagName('eval')[0].innerHTML, 117 | symbol: node.parentElement.getElementsByTagName('eval')[0].innerHTML.charAt(0) 118 | } 119 | 120 | if (currentEval.symbol == '#') { 121 | currentEval.value = currentColor == 'white' ? - CHECKMATE_IN_X_MOVES_VALUE : CHECKMATE_IN_X_MOVES_VALUE; 122 | } 123 | else { 124 | currentEval = { 125 | textValue: currentEval.textValue, 126 | symbol: currentEval.symbol, 127 | value: (currentEval.symbol == '+') ? parseFloat(currentEval.textValue.substring(1)) : 0 - parseFloat(currentEval.textValue.substring(1)) 128 | } 129 | } 130 | 131 | let delta = currentEval.value - previousEval.value; 132 | 133 | let moveText = node.innerHTML; 134 | 135 | if ("white" === currentColor) { 136 | if (delta >= GOOD_MOVE_THRESOLD && delta < EXCELLENT_MOVE_THRESOLD) { 137 | node.innerHTML = ''+ 138 | moveText+'!?'+ 139 | ''; 140 | 141 | goodMoves.white.good++; 142 | } 143 | if (delta >= EXCELLENT_MOVE_THRESOLD && delta < BRILLANT_MOVE_THRESOLD) { 144 | node.innerHTML = ''+ 145 | moveText+'!'+ 146 | ''; 147 | 148 | goodMoves.white.excellent++; 149 | } 150 | if (delta >= BRILLANT_MOVE_THRESOLD) { 151 | node.innerHTML = ''+ 152 | moveText+'!!'+ 153 | ''; 154 | 155 | goodMoves.white.brillant++; 156 | } 157 | } 158 | 159 | if ("black" === currentColor) { 160 | if (delta <= -GOOD_MOVE_THRESOLD && delta > -EXCELLENT_MOVE_THRESOLD) { 161 | node.innerHTML = ''+ 162 | moveText+'!?'+ 163 | ''; 164 | 165 | goodMoves.black.good++; 166 | } 167 | if (delta <= -EXCELLENT_MOVE_THRESOLD && delta > -BRILLANT_MOVE_THRESOLD) { 168 | node.innerHTML = ''+ 169 | moveText+'!'+ 170 | ''; 171 | 172 | goodMoves.black.excellent++; 173 | } 174 | if (delta <= -BRILLANT_MOVE_THRESOLD) { 175 | node.innerHTML = ''+ 176 | moveText+'!!'+ 177 | ''; 178 | 179 | goodMoves.black.brillant++; 180 | } 181 | } 182 | 183 | previousEval = currentEval; 184 | 185 | } 186 | }) 187 | } 188 | } 189 | }); 190 | 191 | return moves; 192 | } 193 | 194 | function handleOpeningMove(node, opening, currentColor) { 195 | const moveText = node.innerHTML; 196 | 197 | node.innerHTML = ''+ 198 | moveText+ 199 | ' '+ 200 | ''+ 201 | '
'+opening.name+''; 202 | 203 | node.parentElement.title = opening.name; 204 | 205 | goodMoves[currentColor].book++; 206 | } 207 | 208 | function checkColor(index) { 209 | if (0 === index%2) { 210 | return "white"; 211 | } 212 | if (0 !== index%2) { 213 | return "black"; 214 | } 215 | } 216 | 217 | function createPgnMoves(moves) { 218 | 219 | let pgn = ''; 220 | 221 | moves.forEach((move, index) => { 222 | if ("white" === checkColor(index)) { 223 | pgn += (index/2+1) + '. ' + move; 224 | } 225 | if ("black" === checkColor(index)) { 226 | pgn += ' ' + move + ' '; 227 | } 228 | }); 229 | 230 | return pgn; 231 | } 232 | 233 | function showDataInTable() { 234 | const inaccuracyRows = document.querySelectorAll('[data-symbol="?!"]'); 235 | 236 | const whiteInaccuracies = inaccuracyRows[0]; 237 | const blackInaccuracies = inaccuracyRows[1]; 238 | 239 | const whiteTable = whiteInaccuracies.parentElement; 240 | const blackTable = blackInaccuracies.parentElement; 241 | 242 | /** 243 | * White separator 244 | */ 245 | const trWhiteSeparator = document.createElement('tr'); 246 | trWhiteSeparator.classList.add('symbol'); 247 | trWhiteSeparator.setAttribute('data-color', 'white'); 248 | trWhiteSeparator.setAttribute('data-symbol', '--'); 249 | const tdWhiteSeparator = document.createElement('td') 250 | tdWhiteSeparator.innerHTML = "-"; 251 | const thWhiteSeparator = document.createElement('th') 252 | thWhiteSeparator.innerHTML = '---'; 253 | trWhiteSeparator.append(tdWhiteSeparator) 254 | trWhiteSeparator.append(thWhiteSeparator); 255 | whiteTable.prepend(trWhiteSeparator); 256 | 257 | /** 258 | * White book 259 | */ 260 | const trWhiteBook = document.createElement('tr'); 261 | trWhiteBook.classList.add('symbol'); 262 | trWhiteBook.setAttribute('data-color', 'white'); 263 | trWhiteBook.setAttribute('data-symbol', 'Book'); 264 | const tdWhiteBook = document.createElement('td') 265 | tdWhiteBook.innerHTML = goodMoves.white.book; 266 | const thWhiteBook = document.createElement('th') 267 | thWhiteBook.innerHTML = 'Book moves'; 268 | trWhiteBook.append(tdWhiteBook) 269 | trWhiteBook.append(thWhiteBook); 270 | whiteTable.prepend(trWhiteBook); 271 | 272 | /** 273 | * White good 274 | */ 275 | const trWhiteGood = document.createElement('tr'); 276 | trWhiteGood.classList.add('symbol'); 277 | trWhiteGood.setAttribute('data-color', 'white'); 278 | trWhiteGood.setAttribute('data-symbol', '!?'); 279 | const tdWhiteGood = document.createElement('td') 280 | tdWhiteGood.innerHTML = goodMoves.white.good; 281 | const thWhiteGood = document.createElement('th') 282 | thWhiteGood.innerHTML = 'Good moves'; 283 | trWhiteGood.append(tdWhiteGood) 284 | trWhiteGood.append(thWhiteGood); 285 | whiteTable.prepend(trWhiteGood); 286 | 287 | /** 288 | * White excellent 289 | */ 290 | const trWhiteExcellent = document.createElement('tr'); 291 | trWhiteExcellent.classList.add('symbol'); 292 | trWhiteExcellent.setAttribute('data-color', 'white'); 293 | trWhiteExcellent.setAttribute('data-symbol', '!'); 294 | const tdWhiteExcellent = document.createElement('td') 295 | tdWhiteExcellent.innerHTML = goodMoves.white.brillant; 296 | const thWhiteExcellent = document.createElement('th') 297 | thWhiteExcellent.innerHTML = 'Excellent moves'; 298 | trWhiteExcellent.append(tdWhiteExcellent) 299 | trWhiteExcellent.append(thWhiteExcellent); 300 | whiteTable.prepend(trWhiteExcellent); 301 | 302 | /** 303 | * White brillancies 304 | */ 305 | const trWhiteBrillancies = document.createElement('tr'); 306 | trWhiteBrillancies.classList.add('symbol'); 307 | trWhiteBrillancies.setAttribute('data-color', 'white'); 308 | trWhiteBrillancies.setAttribute('data-symbol', '!!'); 309 | const tdWhiteBrillancies = document.createElement('td') 310 | tdWhiteBrillancies.innerHTML = goodMoves.white.brillant; 311 | const thWhiteBrillancies = document.createElement('th') 312 | thWhiteBrillancies.innerHTML = 'Brillancies'; 313 | trWhiteBrillancies.append(tdWhiteBrillancies) 314 | trWhiteBrillancies.append(thWhiteBrillancies); 315 | whiteTable.prepend(trWhiteBrillancies); 316 | 317 | /** 318 | * Black separator 319 | */ 320 | const trBlackSeparator = document.createElement('tr'); 321 | trBlackSeparator.classList.add('symbol'); 322 | trBlackSeparator.setAttribute('data-color', 'black'); 323 | trBlackSeparator.setAttribute('data-symbol', '--'); 324 | const tdBlackSeparator = document.createElement('td') 325 | tdBlackSeparator.innerHTML = "-"; 326 | const thBlackSeparator = document.createElement('th') 327 | thBlackSeparator.innerHTML = '---'; 328 | trBlackSeparator.append(tdBlackSeparator) 329 | trBlackSeparator.append(thBlackSeparator); 330 | blackTable.prepend(trBlackSeparator); 331 | 332 | /** 333 | * Black book 334 | */ 335 | const trBlackBook = document.createElement('tr'); 336 | trBlackBook.classList.add('symbol'); 337 | trBlackBook.setAttribute('data-color', 'black'); 338 | trBlackBook.setAttribute('data-symbol', 'Book'); 339 | const tdBlackBook = document.createElement('td') 340 | tdBlackBook.innerHTML = goodMoves.black.book; 341 | const thBlackBook = document.createElement('th') 342 | thBlackBook.innerHTML = 'Book moves'; 343 | trBlackBook.append(tdBlackBook) 344 | trBlackBook.append(thBlackBook); 345 | blackTable.prepend(trBlackBook); 346 | 347 | /** 348 | * Black good 349 | */ 350 | const trBlackGood = document.createElement('tr'); 351 | trBlackGood.classList.add('symbol'); 352 | trBlackGood.setAttribute('data-color', 'black'); 353 | trBlackGood.setAttribute('data-symbol', '!?'); 354 | const tdBlackGood = document.createElement('td') 355 | tdBlackGood.innerHTML = goodMoves.black.good; 356 | const thBlackGood = document.createElement('th') 357 | thBlackGood.innerHTML = 'Good moves'; 358 | trBlackGood.append(tdBlackGood) 359 | trBlackGood.append(thBlackGood); 360 | blackTable.prepend(trBlackGood); 361 | 362 | /** 363 | * Black excellent 364 | */ 365 | const trBlackExcellent = document.createElement('tr'); 366 | trBlackExcellent.classList.add('symbol'); 367 | trBlackExcellent.setAttribute('data-color', 'black'); 368 | trBlackExcellent.setAttribute('data-symbol', '!'); 369 | const tdBlackExcellent = document.createElement('td') 370 | tdBlackExcellent.innerHTML = goodMoves.black.brillant; 371 | const thBlackExcellent = document.createElement('th') 372 | thBlackExcellent.innerHTML = 'Excellent moves'; 373 | trBlackExcellent.append(tdBlackExcellent) 374 | trBlackExcellent.append(thBlackExcellent); 375 | blackTable.prepend(trBlackExcellent); 376 | 377 | /** 378 | * Black brillancies 379 | */ 380 | const trBlackBrillancies = document.createElement('tr'); 381 | trBlackBrillancies.classList.add('symbol'); 382 | trBlackBrillancies.setAttribute('data-color', 'black'); 383 | trBlackBrillancies.setAttribute('data-symbol', '!!'); 384 | const tdBlackBrillancies = document.createElement('td') 385 | tdBlackBrillancies.innerHTML = goodMoves.black.brillant; 386 | const thBlackBrillancies = document.createElement('th') 387 | thBlackBrillancies.innerHTML = 'Brillancies'; 388 | trBlackBrillancies.append(tdBlackBrillancies) 389 | trBlackBrillancies.append(thBlackBrillancies); 390 | blackTable.prepend(trBlackBrillancies); 391 | } 392 | 393 | function lichessGoodMoves(ecoCodes) { 394 | 395 | console.log('Lichess Good Moves successfully started.'); 396 | 397 | /** 398 | * Load moves 399 | */ 400 | 401 | loadMoves(ecoCodes); 402 | showDataInTable(); 403 | } 404 | 405 | // Start the app ! 406 | loadEcoCodesApi(); 407 | 408 | } // check if users is in analysis page 409 | }, false); // addEventListener('load', callback) 410 | })(); // Immediately-Invoked Function Expression (function() {})()) 411 | --------------------------------------------------------------------------------