Automagic Jigidi Solver

Fastest way of solving Jigidi puzzles

// ==UserScript==
// @name        Automagic Jigidi Solver
// @description Fastest way of solving Jigidi puzzles
// @match       *://jigidi.com/*
// @match       *://www.jigidi.com/*
// @grant       GM_xmlhttpRequest
// @version     1.5.13
// @author      Louhikoru
// @namespace   https://greasyfork.org/en/scripts/394279
// @homepageURL https://greasyfork.org/en/scripts/394279
// @supportURL  https://greasyfork.org/en/scripts/394279/feedback
// @license     The Coffeeware License
// @run-at      document-start
// ==/UserScript==
(function() {
    'use strict';

    const BLOCKED = 'javascript/blocked'
    var game_js_src = ''
    var game_js_ver = '3.1824'

    function addScript(text) {
        var head = document.getElementsByTagName("head")[0] || document.documentElement
        var script = document.createElement('script')
        script.type = "text/javascript"
        script.appendChild(document.createTextNode(text))
        head.appendChild(script)
    }

    function modifyUI() {
        /* Add "Solve" button to UI
         * ge() is defined by Jigidi UI and it resolves to document.getElementById()
         */
        var zoom_in = ge("game-zoom-in")
        var solve = document.createElement("button")
        solve.id = "solve"
        solve.title = "Solve"
        solve.className = "btn"
        /* Old "heart" was a embedded GIF which doesn't scale as smoothly as SVG.
         * Jigidi is using Fontello webfonts for most of symbols, embedded WOFF in CSS.
         * To have same style, this new "heart" is derivated from Fontello Modern Pictograms Heart.
         * SVG is rounded to integers and rewritten by hand to minimize its size.
         */
        solve.style.backgroundImage= "url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' viewBox='-230 -300 1200 "+
            "1100'><path d='m380 665l-285-283q-48-47-72-101q-23-50-23-99q0-54 23-95t65-64t96-22q46 0 79 13q29 12 57 37q16 15 51 55l10 12l10"+
            "-12q34-39 51-55q28-25 57-37q34-13 80-13q54 0 96 23t64 64t23 96q0 49-23 99q-24 54-72 101z'/></svg>\")"
        zoom_in.parentNode.insertBefore(solve, zoom_in)

        /* Add "Copy" button to completion message dialog */
        var dialog = ge("completion-message")
        var buttons = dialog.getElementsByTagName('button')
        var copy = document.createElement("button")
        copy.id = "copy";
        copy.title = "Copy to clipboard"
        copy.className = "close"
        copy.innerHTML = "Copy"
        copy.style.marginRight = "10px"
        dialog.insertBefore(copy, buttons[0])
        /* setClickHandler is defined by Jigidi UI to simplify event handling */
        setClickHandler(copy, function(){
            /* Clipboard copying requires a user action for security reasons */
            var message = ge("message-content").innerHTML
            var fail = function() {
                alert('Clipboard copying failed!')
            }
            var listener = function(e) {
                e.preventDefault()
                e.clipboardData ? e.clipboardData.setData('text/plain', message) :
                document.clipboardData ? document.clipboardData.setData('Text', message) : fail()
            }
            document.addEventListener('copy', listener)
            document.execCommand('copy') || fail()
            document.removeEventListener('copy', listener)
        })

        /* This is UI side of Jigidi solver */
        jigidi.solver_run = 0
        jigidi.solve = function(run){
            var solve = ge("solve").style
            jigidi.solver_run = run
            if(run){
                solve.backgroundColor = "#fc5b1f"
                jigidi.solver()
            }else{
                solve.backgroundColor = ""
            }
        }
        setClickHandler(solve, function(){
            jigidi.solve(!jigidi.solver_run)
        })
    }

    /* As this userscript runs before almost anything we are using MutationObserver to watch for changes being made to the DOM tree.
     * Be careful with MutationObserver! As we are monitoring DOM document and all of its children for additions and removals of elements
     * we are receiving a lot of calls.
     */
    new MutationObserver((mutations, observer) => {
        mutations.forEach(({ addedNodes }) => {
            addedNodes.forEach(node => {
                /* We are only interested in <SCRIPT> tags */
                if(node.nodeType === 1 && node.tagName === 'SCRIPT') {
                    /* Inline script does not have location, make it empty */
                    const src = node.src || ''
                    const mimetype = node.type

                    /* Browsers have a fixed list of acceptable MIME types for Javascript.
                     * To block browser from executing the game script we are changing its MIME type.
                     * Just to be sure make the change once and only once. This blocks the script
                     * execution in Safari, Chrome, Edge & IE
                     */
                    if(/game\/js\/release\.js/.test(src) && (!mimetype || mimetype !== BLOCKED)) {
                        node.type = BLOCKED

                        /* Firefox has this additional event which prevents scripts from beeing executed */
                        const beforeScriptExecuteListener = function (event) {
                            if(node.getAttribute('type') === BLOCKED) {
                                event.preventDefault()
                                event.stopPropagation()
                            }
                            node.removeEventListener('beforescriptexecute', beforeScriptExecuteListener)
                        }
                        node.addEventListener('beforescriptexecute', beforeScriptExecuteListener)

                        /* Remember the exact URL of game script */
                        game_js_src = node.src

                        /* Remove the node from the DOM */
                        node.parentElement && node.parentElement.removeChild(node)
                    } else if(game_js_src && /new\sJigidi/.test(node.textContent)) {
                        /* For performance reasons only after we have seen and blocked the game script, only then we try to find Jigidi UI script.
                         * Most reliable way is to detect "new Jigidi" construction. Copy the whole JS to variable so that we can inject it later.
                         * Let the browser execute empty script and cleanup resources
                         */
                        var ui_js = node.textContent
                        node.textContent = ''
                        node.parentElement && node.parentElement.removeChild(node)

                        /* Now we have blocked the game and UI scripts. Use XHR to get the game script. Browsers hide/ignore this re-request as
                         * it is for same resource in same context; content is in browser cache. No additional latency and onload() is likely
                         * executed synchronously.
                         */
                        GM_xmlhttpRequest({
                            method: "GET",
                            url: game_js_src,
                            onload: function(response) {
                                var game_js = response.responseText
                                if (!game_js) {
                                    alert("Failed to load script!")
                                    return
                                }

                                /* Now we have game script in game_js and UI script ui_js. Inject our solver implementation.
                                 * First call to Jigidi.solver() is made by UI after you click the heart button.
                                 * Solver solves the puzzle one piece per call. It calls itself again if the puzzle is
                                 * not complete or you have not clicked the heart button for second time to stop solving.
                                 * setTimeout() is used to stop the size of the call stack growing too large.
                                 *
                                 * Jigidi UI doesn't have direct access to internal state of puzzle. Jigidi class is a wrapper around main state class.
                                 * We could make it public to UI but there are so many additional functions that it is easier to work inside.
                                 * Again to maintain exact style of Jigidi, our internal code is also obfuscated as the original:
                                 *
                                 * Let "f" be flag to tell that we have successfully connected a piece to another piece.
                                 * Let "B" be Jigidi board and "D" be Jigidi board data.
                                 * Call B.Zb(tp) function that gets a sorted array of pieces and calls "tp" function for each in loop.
                                 * Loop execution stops as soon as "tp" function return 0 (or false) which indicates success in our case.
                                 * Sorted array has pieces in ascending order based on the size of piece group, singles first.
                                 * Our function tp(c) calls our function tc() to try connect a piece to one of its neighbors
                                 * in order left, up, right and down. In function tc() let n call cb() with board data,
                                 * col and row to calculate index of neighbor. If n is less than zero, it's out of bounds.
                                 * Call n=D.N(n) to get piece from piece index. Check that pieces don't belong to same piece group;
                                 * that is already "connected" somehow. Calculate canvas delta distances to move piece next to its neightbor.
                                 * D.H tells size of piece in pixels.
                                 *
                                 * This next part emulates the calls normally generated by UI to move a piece: Grab the piece B.Pc(),
                                 * move the piece B.Sc(), release the piece B.nc(), try to connect B.ed() and refresh/redraw the UI b.b.update().
                                 * Check if we succeed in connecting pieces by comparing piece group ids. If we did, set flag "f", and stop trying.
                                 * Logic here is that we will always succeed in connecting two piece until the board is complete.
                                 *
                                 * There are other ways of implementing parts of this, but this is as minimal as it can be.
                                 * Jigidi uses Websocket to communicate each step to server, and server side maintains and monitors
                                 * state changes. It is possible that they will introduce speed limits but that is not a big problem.
                                 * We can also introduce randomness to movement/timings to better mimic human.
                                 */
                                //game_js = game_js.replace("e.logToConsole","true")
                                game_js = game_js.replace("this.getLeaderboard","this.solver=function(){if(!this.solver_run)return;let f=0,B=b.T.u,D=B.data;function "+
                                                          "tc(c,dx,dy){var n=cb(D,c.index.x+dx,c.index.y+dy);if(n<0)return 1;n=D.N(n);if(c.group.id===n.group.id)return 1;"+
                                                          "dx=n.position.x-dx*D.H.width;dy=n.position.y-dy*D.H.height;B.Pc(c.id);B.Sc(c.id,dx,dy);B.nc(c.id);B.ed(c.id);"+
                                                          "b.b.update();return!(f=c.group.id===n.group.id)}function tp(c){return tc(c,-1,0)&&tc(c,0,1)&&tc(c,1,0)&&tc(c,0,-1)}"+
                                                          "B.Zb(tp);f?setTimeout(function(){this.solver();}.bind(this),0):this.solve(0)};this.getLeaderboard")
                                addScript(game_js)
                                addScript(ui_js)

                                /* Override loadComplete function to avoid leaderboard issues */
                                jigidi.loadComplete = (function(_super){
                                    return function(){
                                        var info = jigidi.buildInfo()
                                        if(info && info.version === game_js_ver){
                                            var cfg = jigidi.config()
                                            if(cfg && cfg.guest){
                                                modifyUI()
                                            }else{
                                                showCompletionMessage("<b>Automagic Jigidi Solver disabled!</b><br>"+
                                                                      "To use it, sign out from Jigidi and try again.")
                                            }
                                        }else{
                                            showCompletionMessage("<b>Automagic Jigidi Solver disabled!</b><br>"+
                                                                  "Jigidi has updated to v" + info.version + "<br>"+
                                                                  "but solver supports v" + game_js_ver + "<br>"+
                                                                  "Try updating the userscript and then try again.")
                                        }
                                        _super.apply(this, arguments)
                                    }
                                })(jigidi.loadComplete)
                            }
                        })

                        /* If you are reading this, it should be obvious what is going on here.
                         * We should exclude/ignore same of Jigidi pages to minimize effect of this userscript.
                         */
                        observer.disconnect()
                    }
                }
            })
        })
    }).observe(document, { childList: true, subtree: true })
})();