sealldeveloper / Lichess - Detailed Moves

// ==UserScript==
// @name            Lichess - Detailed Moves
// @license         GPL-3.0-only 
// @namespace       https://github.com/sealldeveloper/lichess-better-moves
// @contributionURL https://github.com/sealldeveloper/lichess-better-moves
// @version         0.5
// @description     Show brillant, excellent, great and book moves on lichess.org as chess.com does, an updated version of Thomas Sihapanya's version.
// @author          Seall.DEV & Thomas Sihapnya
// @require         https://greasyfork.org/scripts/47911-font-awesome-all-js/code/Font-awesome%20AllJs.js?version=275337
// @include         /^https\:\/\/lichess\.org\/[a-zA-Z0-9]{8,}/
// @grant           GM.xmlHttpRequest
// @grant           unsafeWindow
// @inject-into     content
// ==/UserScript==
// ==OpenUserJS==
// @author          sealldeveloper
// ==/OpenUserJS==

(function() {
    'use strict';
    const GOOD_MOVE_THRESOLD = 0.6;
    const EXCELLENT_MOVE_THRESOLD = 1;
    const BRILLANT_MOVE_THRESOLD = 2;
    const CHECKMATE_IN_X_MOVES_VALUE = 100;

    let goodMoves = {
        white : {
            'book' : 0,
            'good' : 0,
            'excellent': 0,
            'brillant': 0,
        },
        black : {
            'book' : 0,
            'good' : 0,
            'excellent': 0,
            'brillant': 0,
        }
    }


    /**
     * Wait for the page to load all its elements before running the script
     */
    window.addEventListener('load', function() {

        /**
         * Create click event since .click on elements doesn't work quite well in Chrome
         */
        let clickEvent = document.createEvent('MouseEvents');
        clickEvent.initMouseEvent('mousedown', true, true);

        /**
         * Moves explorer must be opened to detect book moves. If not, user is prompted to run an analysis and reload the page.
         */
        const formAnalysis = document.getElementsByClassName('future-game-analysis')[0];

        if (typeof(formAnalysis) !== 'undefined') {
            alert('Lichess Detailed 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) !');
        }

        /**
         * Check if users is in analysis page and analysis has been run
         */
        if (document.getElementsByClassName('analyse__tools').length > 0
            && document.getElementsByClassName('future-game-analysis').length === 0
            && document.getElementsByClassName('computer-analysis active').length > 0) {

            const ecoCodesApiUrl = 'https://github.com/sealldeveloper/lichess-detailed-moves/raw/main/data/eco.json';

            /**
             * Loads ECO codes API
             * https://github.com/sealldeveloper/lichess-detailed-moves/raw/main/data/eco.json
             */
            function loadEcoCodesApi() {
                GM.xmlHttpRequest({
                    method: "GET",
                    url:ecoCodesApiUrl,
                    onload: function(response) {
                        lichessGoodMoves(JSON.parse(response.responseText));
                    },
                    onerror: function(err) {
                        alert('Lichess Detailed Moves: The script cannot be launched (maybe you have forbid the access to a cross-origin resource ?) - Refresh the page if you want to start again.');
                    }
                });
            }

            function loadMoves(ecoCodes) {
                let domMoves = document.getElementsByTagName('move');
                let moves = [];
                let previousEval = {
                    value: '+0.0',
                    symbol: '+',
                    absVal: '0.0'
                };

                Object.values(domMoves).forEach(domMove => {
                    if (!domMove.classList.contains('empty')) {
                        if ("undefined" !== typeof(domMove.childNodes)) {

                            domMove.childNodes.forEach(node => {

                                if ('SAN' === node.tagName) {
                                    /**
                                     * Handle opening
                                     */
                                    moves.push(node.innerHTML);

                                    let currentColor = checkColor(moves.length-1);
                                    let currentPgn = createPgnMoves(moves);
                                    let foundOpening = ecoCodes.find(eco => eco.moves.toLowerCase().trim() == currentPgn.toLowerCase().trim());

                                    if (typeof(foundOpening) !== 'undefined') {
                                        handleOpeningMove(node, foundOpening, currentColor);
                                    }

                                    /**
                                     * Handle evaluation
                                     */
                                 		if (!node.innerHTML.endsWith('#')){
                                      let currentEval = {
                                          textValue: domMove.getElementsByTagName('eval')[0].innerHTML,
                                          symbol: domMove.getElementsByTagName('eval')[0].innerHTML.charAt(0)
                                      }

                                      if (currentEval.symbol == '#') {
                                          currentEval.value = currentColor == 'white' ? - CHECKMATE_IN_X_MOVES_VALUE : CHECKMATE_IN_X_MOVES_VALUE;
                                      }
                                      else {
                                          currentEval = {
                                              textValue: currentEval.textValue,
                                              symbol: currentEval.symbol,
                                              value: (currentEval.symbol == '+') ? parseFloat(currentEval.textValue.substring(1)) : 0 - parseFloat(currentEval.textValue.substring(1))
                                          }
                                      }

                                      let delta = currentEval.value - previousEval.value;

                                      let moveText = node.innerHTML;

                                      if ("white" === currentColor) {
                                          if (delta >= GOOD_MOVE_THRESOLD && delta < EXCELLENT_MOVE_THRESOLD) {
                                              node.innerHTML = '<span style="color: #b2f196;">'+
                                                                      moveText+'!?'+
                                                              '</span>';

                                              goodMoves.white.good++;
                                          }
                                          if (delta >= EXCELLENT_MOVE_THRESOLD && delta < BRILLANT_MOVE_THRESOLD) {
                                              node.innerHTML = '<span style="color: #96bc4b;">'+
                                                                      moveText+'!'+
                                                              '</span>';

                                              goodMoves.white.excellent++;
                                          }
                                          if (delta >= BRILLANT_MOVE_THRESOLD) {
                                              node.innerHTML = '<span style="color: #1baca6;">'+
                                                                      moveText+'!!'+
                                                              '</span>';

                                              goodMoves.white.brillant++;
                                          }
                                      }

                                      if ("black" === currentColor) {
                                          if (delta <= -GOOD_MOVE_THRESOLD && delta > -EXCELLENT_MOVE_THRESOLD) {
                                              node.innerHTML = '<span style="color: #b2f196;">'+
                                                                      moveText+'!?'+
                                                              '</span>';

                                              goodMoves.black.good++;
                                          }
                                          if (delta <= -EXCELLENT_MOVE_THRESOLD && delta > -BRILLANT_MOVE_THRESOLD) {
                                              node.innerHTML = '<span style="color: #96bc4b;">'+
                                                                      moveText+'!'+
                                                              '</span>';

                                              goodMoves.black.excellent++;
                                          }
                                          if (delta <= -BRILLANT_MOVE_THRESOLD) {
                                              node.innerHTML = '<span style="color: #1baca6;">'+
                                                                      moveText+'!!'+
                                                              '</span>';

                                              goodMoves.black.brillant++;
                                          }
                                      }

                                      previousEval = currentEval;
                                    }

                                }
                            })
                        }
                    }
                });

                return moves;
            }

            function handleOpeningMove(node, opening, currentColor) {
                const moveText = node.innerHTML;

                node.innerHTML = '<span style="color: #a88865;">'+
                                        moveText+
                                        ' <i class="fas fa-book" style="font-size: 0.7em"></i>'+
                                '</span>'+
                                '<pre style="font-size:0.7em; width:0">'+opening.name+'</pre>';

                node.parentElement.title = opening.name;

                goodMoves[currentColor].book++;
            }

            function checkColor(index) {
                if (0 === index%2) {
                    return "white";
                }
                if (0 !== index%2) {
                    return "black";
                }
            }

            function createPgnMoves(moves) {

                let pgn = '';

                moves.forEach((move, index) => {
                    if ("white" === checkColor(index)) {
                        pgn += (index/2+1) + '. ' + move;
                    }
                    if ("black" === checkColor(index)) {
                        pgn += ' ' + move + ' ';
                    }
                });

                return pgn;
            }

            function showDataInTable() {

                const whiteTable = document.getElementsByClassName('advice-summary__side')[0]
                const blackTable = document.getElementsByClassName('advice-summary__side')[1]
              	function dataPoint(colour,symbol,data,text,table,coloured) {
                    var first = true
                    table.childNodes.forEach(node => {
                        if (node.innerHTML.includes('inaccuracies') && first === true) {
                            const before=node
                            const div = document.createElement('div');
                            if (data !== 0){
                                div.style='color: '+coloured;
                            }
                            div.classList.add('symbol');
                            div.classList.add('advice-summary__error');
                            div.setAttribute('data-color', colour);
                            div.setAttribute('data-symbol', symbol);
                            const strong = document.createElement('strong')
                            strong.innerHTML = data;
                            div.append(strong)
                            div.innerHTML = div.innerHTML+text;
                            table.insertBefore(div,before);
                            first = false
                        }
                    })
                }
                // White
              	dataPoint('white','!!',goodMoves.white.brillant,' brilliancies',whiteTable,'#1baca6')   
              	dataPoint('white','!',goodMoves.white.excellent,' excellencies',whiteTable,'#96bc4b')
              	dataPoint('white','!?',goodMoves.white.good,' greats',whiteTable,'#b2f196')
              	dataPoint('white','Book',goodMoves.white.book,' book',whiteTable,'#a88865')
			
                // Black
                dataPoint('black','!!',goodMoves.black.brillant,' brilliancies',blackTable,'#1baca6')
              	dataPoint('black','!',goodMoves.black.excellent,' excellencies',blackTable,'#96bc4b')
              	dataPoint('black','!?',goodMoves.black.good,' greats',blackTable,'#b2f196')
              	dataPoint('black','Book',goodMoves.black.book,' book',blackTable,'#a88865')
              	
                
            }

            function lichessGoodMoves(ecoCodes) {

                console.log('Lichess Detailed Moves: Successfully started!');

                loadMoves(ecoCodes);
                showDataInTable();
            }

            // Start the app !
            loadEcoCodesApi();

        } // check if users is in analysis page
    }, false); // addEventListener('load', callback)
})(); // Immediately-Invoked Function Expression (function() {})())