Wiki Summary

Display Wikipedia summary of the Geoguessr locations. Works with streaks, single player 5 round games and challenges.

// ==UserScript==
// @name         Wiki Summary
// @include      /^(https?)?(\:)?(\/\/)?([^\/]*\.)?geoguessr\.com($|\/.*)/
// @version      0.6.0
// @description  Display Wikipedia summary of the Geoguessr locations. Works with streaks, single player 5 round games and challenges.
// @author       semihM (aka rhinoooo_), MiniKochi
// @source       https://github.com/semihM/GeoGuessrScripts/blob/main/WikiSummary
// @supportURL   https://github.com/semihM/GeoGuessrScripts/issues
// @require      http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
// @require      http://code.jquery.com/jquery-3.4.1.min.js
// @grant        GM_addStyle
// @namespace https://greasyfork.org/users/851187
// ==/UserScript==


///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// API KEYS : Get your keys from following sites
// - https://www.bigdatacloud.com/
// - https://opentripmap.io/
//
// After every update, these values will be reset. But since the script stores them as "cookies", they will still be replaced internally
// API Keys are not required to be updated in here after they get removed because of an auto-update
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// BigDataCloud for location information
let BigDataCloud_APIKEY = 'ENTER_API_KEY_HERE'; //Replace ENTER_API_KEY_HERE with yours from https://www.bigdatacloud.com/

// OpenTripMap for places nearby
let OpenTripMap_APIKEY = 'ENTER_API_KEY_HERE'; //Replace ENTER_API_KEY_HERE with yours from https://opentripmap.io/

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SETTINGS: Make sure UseSettingsBelow_InsteadOfCookies is set to true to use settings written here instead of previous one in cookies

// After every update, settings will be reset. But since the script stores them as "cookies", they will still be replaced internally
// There will be alerts prompted in the site after updates, make sure you read them!

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

let Settings = {
    // Maximum fact text length, may exceed the limit if last sentence is long enough
    "MaximumFactMessageLength": 420,

    // Maximum amount of famous place facts to display
    "MaximumPlaceFactCountToDisplay": 5,
    // Maximum amount of facts(geographical + famous place) to display
    "MaximumFactCountToDisplay": 10,

    // Categories for nearby places, check https://opentripmap.io/catalog for other categories. Seperate categories with ',' commas
    "PlaceCategoriesToSearchFor": "historic,cultural,natural,architecture,religion",
    // Radius in meters to search for places nearby
    "PlaceSearchRadiusInMeters": 2000,

    // true: Display facts under the main green continue button; false: Display facts before continue button
    "DisplayFactsBelowButtons": true,

    // Fact's wiki title color for both geographical and famous place facts
    "FactWikiTitleColor": "lime",
    // Fact's wiki text color for both geographical and famous place facts
    "FactWikiTextColor": "white",

    // Geographical fact title
    "GeographyFactTitle": "Geographical",
    // Geographical fact title color name, lowercase
    "GeographyFactTitleColorName": "orange",

    // Famous place fact title
    "FamousPlaceFactTitle": "Famous Place",
    // Famous place fact text color name, lowercase
    "FamousPlaceFactTitleColorName": "cyan",

    // Source link color name, lowercase
    "SourceLinkColorName": "darkgray",

    // true: Display fact number after the title; false: Don't display fact number
    "DisplayFactNumber": true,

    // true: Open wiki links in new tab; false: Open in current tab
    "OpenInNewTab": true,

    // Exclude given wiki id's from facts
    "ExcludedWikiPageIds": [
        // Remove the first '//' before the wiki ids ( //12345, -> 12345, ) to exclude the wiki page from results
        // Add more by adding a ',' comma after the previous wiki id
        //83759, // USA
        //13530298, // UK
    ]
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

const DEBUG_ENABLED = false // true: Console print enabled for debugging; false: Don't print any debug information

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

const API_URL = "https://api.bigdatacloud.net/data/reverse-geocode?localityLanguage=en&"
const WIKI_URL = "https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro&explaintext&redirects=1&origin=*&titles="
const WIKIDATA_URL = "https://www.wikidata.org/w/api.php?action=wbgetentities&format=json&origin=*&props=sitelinks&sitefilter=enwiki&ids="
const OPENTRIP_URL = `https://api.opentripmap.com/0.1/en/places/radius?radius=${Settings.PlaceSearchRadiusInMeters}&limit=${Settings.MaximumPlaceFactCountToDisplay}&src_attr=wikidata&kinds=${encodeURIComponent(Settings.PlaceCategoriesToSearchFor)}&apikey=`

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

const SettingsVersion = 6; // ONLY UPDATE WHILE RELEASING A NEW VERSION

const EMPTYAPIKEY = "ENTER_API_KEY_HERE"
const INVALIDLINK = "#";
const BIGDATACLOUD_APICOOKIE = "geoguessr_script_semihM_bigdatacloudkey"
const OPENTRIPMAP_APICOOKIE = "geoguessr_script_semihMopentripmapkey"
const SETTINGS_COOKIE = "geoguessr_script_semihM_WikiSummarySettings"
const SETTINGS_ASKED_COOKIE = "geoguessr_script_semihM_WikiSummarySettingsAsked"

const _id_fact_div = "location-fact"
const _class_roundResult_5roundGame = "round-result_actions__5j26U"
const _class_roundResult_streakGame = "streak-round-result_root__WxUU9"
const _class_roundResult_Bullseye = "round-score_container__avps2"
const _class_nextButton_Bullseye = "button_button__CnARx"
const _class_correct_loc = 'styles_circle__2tw8L styles_variantFloating__mawbd styles_colorWhite__2QcUQ styles_borderSizeFactorOne__2Di08'

const SummaryLoadingPlaceHolderInnerHtml = `<div id="${_id_fact_div}" style="text-align:center"><br><br>Loading wikipedia summaries...</div><br>`

const CookieDays = 365

let checked = parseInt(sessionStorage.getItem("FactLocationChecked"), 10);
let facts = []

let placeWikidataTitles = []
let needsWiki = true;

if (sessionStorage.getItem("FactLocationChecked") == null) {
    sessionStorage.setItem("FactLocationChecked", 0);
    checked = 0;
};

/////////////////////////
// Cookies
/////////////////////////
CheckCookiesForAPIKeys()
CheckCookiesForSettings()
    /////////////////////////

function debug(obj) {
    if (DEBUG_ENABLED) console.log(obj)
}

function setCookie(name, value, days) {
    var expires = "";
    if (days) {
        var date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
        expires = "; expires=" + date.toUTCString();
    }
    document.cookie = name + "=" + (value || "") + expires + "; path=/";
}

function getCookie(name) {
    var nameEQ = name + "=";
    var ca = document.cookie.split(';');
    for (var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
    }
    return null;
}

function GetSettingsString() {
    return JSON.stringify(Settings)
}

function CheckCookiesForAPIKeys() {
    let key = ""
    if (BigDataCloud_APIKEY == EMPTYAPIKEY) {
        if (key = getCookie(BIGDATACLOUD_APICOOKIE)) BigDataCloud_APIKEY = key
        else if ((key = prompt("Couldn't find bigdatacloud.com API key, please enter your key")) != "") setCookie(BIGDATACLOUD_APICOOKIE, BigDataCloud_APIKEY = key, CookieDays);
        else return alert("Failed to initialize WikiSummary script. Make sure to add your key manually!")
    } else setCookie(BIGDATACLOUD_APICOOKIE, BigDataCloud_APIKEY, CookieDays)

    if (OpenTripMap_APIKEY == EMPTYAPIKEY) {
        if (key = getCookie(OPENTRIPMAP_APICOOKIE)) OpenTripMap_APIKEY = key
        else if ((key = prompt("Couldn't find opentripmap.io API key, please enter your key")) != "") setCookie(OPENTRIPMAP_APICOOKIE, OpenTripMap_APIKEY = key, CookieDays);
        else return alert("Failed to initialize WikiSummary script. Make sure to add your key manually!")
    } else setCookie(OPENTRIPMAP_APICOOKIE, OpenTripMap_APIKEY, CookieDays)
}

function CheckCookiesForSettings() {
    let settings = getCookie(SETTINGS_COOKIE)
    let asked = getCookie(SETTINGS_ASKED_COOKIE);

    if (settings == null || asked == null) // First time
    {
        setCookie(SETTINGS_COOKIE, JSON.stringify(Settings), CookieDays)
        setCookie(SETTINGS_ASKED_COOKIE, SettingsVersion, CookieDays)
    } else {
        let cookieSettings = JSON.parse(settings)
        if (asked != SettingsVersion) // There was an update
        {
            let restore = window.confirm("There was an update to WikiSummary(by rhino). Would you like to restore the old settings ? Click \"Cancel\" if you havent made any changes and use default settings.");

            setCookie(SETTINGS_ASKED_COOKIE, SettingsVersion, CookieDays)

            if (restore) {
                for (const [key, value] of Object.entries(Settings)) {
                    if (!(key in cookieSettings)) {
                        cookieSettings[key] = value
                    }
                }

                let cookiesets = JSON.stringify(cookieSettings)
                setCookie(SETTINGS_COOKIE, cookiesets, CookieDays) // Use from cookies

                let jsonframe = document.createElement("pre")
                jsonframe.innerHTML = "<pre>// Setting start around line 40\n// v COPY STARTING FROM THE LINE BELOW v\nlet Settings = " + JSON.stringify(cookieSettings, undefined, 2) + "</pre>"

                let myDialog = document.createElement("dialog");
                document.body.appendChild(myDialog)

                myDialog.appendChild(jsonframe);

                let closeBtn = document.createElement("p")
                closeBtn.style = "background-color: red; color: white; font-size:18px; border: 2px solid black; width: auto; text-align:center;"
                closeBtn.textContent = "Click here to close this frame"
                closeBtn.onclick = () => myDialog.remove()

                myDialog.appendChild(closeBtn);
                myDialog.appendChild(jsonframe);

                myDialog.showModal();

                alert("Old settings will be shown in a small window for copying, paste them into the Wiki Summary script!")
            } else {
                setCookie(SETTINGS_COOKIE, JSON.stringify(Settings), CookieDays) // Store new update's settings
            }
        } else {
            setCookie(SETTINGS_COOKIE, JSON.stringify(Settings), CookieDays) // Store currently written settings
        }
    }
}

function cleanPages(pages) {
    // Missing or invalid
    if (-1 in pages) delete pages[-1]
    if (-2 in pages) delete pages[-2]

    Settings.ExcludedWikiPageIds.forEach(idx => idx in pages ? delete pages[idx] : null);
}

function setNameToPostal(obj, name) {
    obj.name = name
    obj.description = name
}

function styleFact(name, desc) {
    if (desc.startsWith(". "))
        desc = desc.substring(2);
    return `<h3 style="color: ${Settings.FactWikiTitleColor}">${name}</h3><br>${desc}`
}

function getTitlesFromLocation() {
    return getLocationObject()
        .then(async loc => {
            debug(loc)

            needsWiki = true;
            placeWikidataTitles = [];

            if (loc == null || !("localityInfo" in loc)) return null

            let infos = loc.localityInfo.informative.concat(loc.localityInfo.administrative.filter(o => o.adminLevel >= 3))
                .sort((firstEl, secondEl) => firstEl.order > secondEl.order ? 1 : -1)

            if (infos.length == 0) return null

            let maxorder = infos[infos.length - 1].order

            debug("]infos")
            debug(infos)

            return await getNearByLocationsFromLatLng(loc.latitude, loc.longitude)
                .then(locs => {
                    debug("]locs")
                    debug(locs)
                    return locs.features.map(place =>
                        "wikidata" in place.properties ? {
                            "order": Math.floor(maxorder + (Math.random() * maxorder) / 2.0),
                            "name": place.properties.name,
                            "description": place.properties.name + "(" + place.properties.kinds + ")",
                            "wikidataId": place.properties.wikidata,
                            "isPlaceFact": true
                        } :
                        null).filter(o => o != null)
                })
                .then(async places => {

                    debug("]places")
                    debug(places)

                    infos = infos.concat(places)
                        .sort((firstEl, secondEl) => firstEl.order > secondEl.order ? 1 : -1);

                    debug("]infos")
                    debug(infos)

                    let len = Object.keys(infos).length;
                    if (len == 1) {
                        let info = infos[0]
                        if (info.order < 3) {
                            needsWiki = false;
                            return styleFact(info.name, info.description)
                        }
                        return info;
                    } else {
                        let filtered = infos.filter(o => o.order >= 3 && "wikidataId" in o);

                        if (filtered.length < Settings.MaximumFactCountToDisplay) {
                            filtered = infos.filter(o => o.order >= 2 && "wikidataId" in o);
                        }

                        filtered.forEach(obj => obj.description == "postal code" ? setNameToPostal(obj, loc.city == "" ? loc.principalSubdivision : loc.city) : null);
                        debug("]filtered infos")
                        debug(filtered)

                        if (filtered.length == 0) {
                            if (infos.length >= 1) {
                                facts = infos.map(obj => { return { "text": styleFact(obj.name, obj.description), "link": INVALIDLINK, "isGeoFact": true } })
                            }
                            needsWiki = false;
                            return null;
                        }

                        let red = []
                        let i = 0;
                        while (i < filtered.length) {
                            let curr = filtered[i];
                            let t = await fetch(WIKIDATA_URL + curr.wikidataId)
                                .then(res => res.json())
                                .then(out =>
                                    (out.success != 1 || "error" in out || !("enwiki" in out.entities[curr.wikidataId].sitelinks)) ?
                                    "" :
                                    processAndGetWikidataTitle(out, curr))

                            if (t != "" && red.indexOf(t) == -1) red.push(t);

                            i++;
                        }

                        return red.reverse().join("|")
                    }
                })
        })
}

function processAndGetWikidataTitle(data, obj) {
    let title = data.entities[obj.wikidataId].sitelinks.enwiki.title;

    if ("isPlaceFact" in obj) {
        if (placeWikidataTitles.indexOf(title) == -1) {
            debug("]place title processed: " + title)
            placeWikidataTitles.push(title)
        }
    } else debug("]geographic title processed: " + title)

    return encodeURIComponent(title)
}

async function btnClick(btn) {
    return new Promise(resolve => btn.onclick = () => resolve());
}

function getCorrectLocationDivForChallenge() {
    return document.querySelector('[alt="Correct location"]').parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement
}

async function getLocationObjectGame() {
    const tag = window.location.href.substring(window.location.href.lastIndexOf('/') + 1)
    const game_endpoint = "https://www.geoguessr.com/api/v3/games/" + tag
    const challenge_endpoint = "https://www.geoguessr.com/api/v3/challenges/" + tag + "/game"
    const api_url = isInChallange() ? challenge_endpoint : game_endpoint

    return fetch(api_url)
        .then(res => res.json())
        .then(out => {
            let guess_counter = out.player.guesses.length

            let lat = out.rounds[guess_counter - 1].lat;
            let lng = out.rounds[guess_counter - 1].lng;

            return getLocationFromLatLng(lat, lng);
        })
}

async function getLocationObjectBullseye() {
    const tag = window.location.href.substring(window.location.href.lastIndexOf('/') + 1)
    const api_url = "https://game-server.geoguessr.com/api/bullseye/" + tag

    return fetch(api_url, { credentials: "include" })
        .then(res => res.json())
        .then(out => {
            let guess_counter = out.players[0].guesses.length

            let lat = out.rounds[guess_counter - 1].panorama.lat;
            let lng = out.rounds[guess_counter - 1].panorama.lng;

            return getLocationFromLatLng(lat, lng);
        })
}

async function getLocationObject() {
    return isInBullseye() ? getLocationObjectBullseye() : getLocationObjectGame()
}

function getNearByLocationsFromLatLng(lat, lng) {
    let api = OPENTRIP_URL + OpenTripMap_APIKEY + "&lat=" + lat + "&lon=" + lng
    return fetch(api)
        .then(res => res.json())
}

function getLocationFromLatLng(lat, lng) {
    let api = API_URL + "latitude=" + lat + "&longitude=" + lng + "&key=" + BigDataCloud_APIKEY
    return fetch(api)
        .then(res => res.json())
}

function getFactFromTitles(titles) {
    return fetch(WIKI_URL + titles)
        .then(res => res.json())
        .then(result => {

            facts = []
            let pages = result.query.pages;
            cleanPages(pages);

            debug("]cleaned pages");
            debug(pages)

            let keys = Object.keys(pages);
            if (keys.length == 0) return null

            let del = 0;

            keys.forEach(k => {
                if (facts.length >= Settings.MaximumFactCountToDisplay) return

                let fact = pages[k];
                if (fact == null) return

                let reduced = fact.extract;
                reduced = reduced.trimEnd("\n")
                if (reduced.endsWith("refer to:")) return

                if (reduced.length > Settings.MaximumFactMessageLength) {
                    let paragraphs = reduced.split(". ")
                    let j = 0
                    reduced = paragraphs.length != 0 ? "" : reduced

                    while (j < paragraphs.length && reduced.length <= Settings.MaximumFactMessageLength) {
                        reduced = reduced + ". " + paragraphs[j]
                        j++;
                    }

                    // TO-DO: Add "read more" button
                    //reduced = reduced.slice(0,Settings.MaximumFactMessageLength) + "..."
                }

                let f = {
                        "text": styleFact(fact.title, reduced),
                        "link": "https://en.wikipedia.org/?curid=" + fact.pageid,
                        "isGeoFact": placeWikidataTitles.indexOf(fact.title) == -1
                    }
                    //debug(f)
                facts.push(f)
            })

            return facts;
        });
}


function SetDisplayFact() {
    getTitlesFromLocation()
        .then(titles => {

            debug("]reduced titles result: " + titles)
            if (needsWiki) {
                getFactFromTitles(titles).then(facts => {
                    if (facts == null) {
                        facts = [{
                            "text": styleFact(titles.name, titles.description),
                            "link": INVALIDLINK,
                            "isGeoFact": true
                        }]
                    }
                    debug(facts)
                    setFactInnerHtml();
                })
            } else {
                needsWiki = true;
                if (titles != null) {
                    facts = [{
                        "text": titles,
                        "link": INVALIDLINK,
                        "isGeoFact": true
                    }]
                }
                setFactInnerHtml();
            }
        })
}

function getFactTitleColor(fact) {
    return fact.isGeoFact ? Settings.GeographyFactTitleColorName : Settings.FamousPlaceFactTitleColorName;
}

function getFactTitle(fact) {
    return fact.isGeoFact ? Settings.GeographyFactTitle : Settings.FamousPlaceFactTitle;
}

function getFactTextHtml(fact) {
    return `<div style="color: ${Settings.FactWikiTextColor}">` + fact.text.split(". ").reduce((prev, curr) => prev + "<br>" + curr) + "</div>";
}

function setFactInnerHtml() {
    const new_tab = Settings.OpenInNewTab ? ` target="_blank" rel="noopener noreferrer"` : ''
    const style_source_text = `color: ${Settings.SourceLinkColorName}; font-size: 12px;`;
    let str = facts
        .map((fact, i) => {
            return `<br><h2 style="color: ${getFactTitleColor(fact)}">${getFactTitle(fact)} Fact ${Settings.DisplayFactNumber ? (i+1) : ""}</h2><span style="${style_source_text}">(</span><u><a href="${fact.link}"${new_tab}; style="${style_source_text}"><i>source</i></a></u><span style="${style_source_text}">)</span><br><div style="text-align: justify;text-justify: inter-word;">${getFactTextHtml(fact)}</div>`
        })
        .join("<hr style='background: var(--ds-color-white-20, hsla(0,0%,100%,0.2)) ; height: .0625em ; border: 0 ; margin: 1rem 0 0.5rem 0'>")

    try {
        document.getElementById(_id_fact_div).innerHTML = str;
    } catch (error) { console.log(error); }
}

// 5 round game round summary div or null
function get5RoundGameSummaryDiv() {
    let div = document.getElementsByClassName(_class_roundResult_5roundGame);
    if (div.length == 0) return null
    else return div[0]
}

function set5RoundGameSummaryDivPlaceHolder() {
    let newDiv1 = document.createElement("div")
    let parent = get5RoundGameSummaryDiv();

    if (Settings.DisplayFactsBelowButtons) parent.parentElement.appendChild(newDiv1);
    else parent.insertBefore(newDiv1, parent.lastElementChild);

    newDiv1.innerHTML = SummaryLoadingPlaceHolderInnerHtml;
}

// Streak game round summary div or null
function getStreakGameSummaryDiv() {
    let div = document.getElementsByClassName(_class_roundResult_streakGame);
    if (div.length == 0) return null
    else return div[0]
}

function setStreakGameSummaryDivPlaceHolder() {
    let newDiv1 = document.createElement("div")
    let parent = getStreakGameSummaryDiv();

    if (Settings.DisplayFactsBelowButtons) parent.parentElement.appendChild(newDiv1);
    else parent.insertBefore(newDiv1, parent.lastElementChild);

    newDiv1.innerHTML = SummaryLoadingPlaceHolderInnerHtml;
}

// Bullseye round summary div or null
function getBullseyeGameSummaryDiv() {
    let div = document.getElementsByClassName(_class_roundResult_Bullseye);
    if (div.length == 0) return null
    else return div[0]
}

function setBullseyeGameSummaryDivPlaceHolder() {
    let newDiv1 = document.createElement("div")
    let parent = getBullseyeGameSummaryDiv();

    if (Settings.DisplayFactsBelowButtons) parent.appendChild(newDiv1);
    else parent.insertBefore(newDiv1, parent.lastElementChild);

    newDiv1.innerHTML = SummaryLoadingPlaceHolderInnerHtml;
}

function getBullseyeButtonDiv() {
    let div = document.getElementsByClassName(_class_nextButton_Bullseye);
    if (div.length == 0) return null
    else return div[0]
}

function setBullseyeButtonStyle() {
    let button = getBullseyeButtonDiv();
    button.style.padding = "var(--vertical-padding, 0.75rem) var(--horizontal-padding, 1.5rem)"
}

function factCheckStateAttempt(newDiv1) {
    if (document.getElementById(_id_fact_div) || !isValidGame() || !isInRoundResultPage()) return

    if (get5RoundGameSummaryDiv()) set5RoundGameSummaryDivPlaceHolder()
    else if (getStreakGameSummaryDiv()) setStreakGameSummaryDivPlaceHolder()
    else if (getBullseyeGameSummaryDiv()) setBullseyeGameSummaryDivPlaceHolder()

    if (getBullseyeButtonDiv()) setBullseyeButtonStyle()
};

function isInChallange() {
    return location.pathname.startsWith("/challenge/");
}

function isInBullseye() {
    return location.pathname.startsWith("/bullseye/");
}

function isInClassicGame() {
    return location.pathname.startsWith("/game/") || isInChallange();
}

function isValidGame() {
    return isInClassicGame() || isInBullseye();
}

function isInRoundResultPage() {
    if (isInClassicGame()) return !!document.querySelector('.result-layout_root__NfX12')
    else if (isInBullseye()) return !!document.querySelector('.round-score_container__avps2')
    return false
}

function isFactAlreadyChecked() {
    return sessionStorage.getItem("FactLocationChecked") != 0
}

function factCheckState() {
    if (isValidGame() && isInRoundResultPage() && !isFactAlreadyChecked()) {
        SetDisplayFact();
        checked = checked + 1;
        sessionStorage.setItem("FactLocationChecked", checked);
    } else if (isValidGame() && !isInRoundResultPage() && isFactAlreadyChecked()) {
        checked = 0;
        sessionStorage.setItem("FactLocationChecked", checked)
    };
}

function tryFactCheck() {
    factCheckState();

    setTimeout(factCheckState, 250);
    setTimeout(factCheckState, 500);
    setTimeout(factCheckState, 1200);
    setTimeout(factCheckState, 2000);

    setTimeout(factCheckStateAttempt, 300);
    setTimeout(factCheckStateAttempt, 500);
    setTimeout(factCheckStateAttempt, 1200);
    setTimeout(factCheckStateAttempt, 2000);
};

document.body.addEventListener('transitionend', () => {
    if (isValidGame() && isInRoundResultPage() != isFactAlreadyChecked())
        tryFactCheck()
});