BlackJackStrat

Script to help with Blackjack.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         BlackJackStrat
// @namespace    http://tampermonkey.net/s
// @version      2025-12-01.8
// @description  Script to help with Blackjack.
// @author       You
// @match        https://www.torn.com/page.php?sid=blackjack
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        none
// ==/UserScript==

(function() {
    "use strict";

    // All code from and credit to torntools

    const ACTIONS = {
        H: "Hit",
        S: "Stand",
        D: "Double Down",
        P: "Split",
        R: "Surrender",
    };

    /* these suggestions come from :
	https://www.beatingbonuses.com/bjstrategy.php?decks=8&soft17=stand&doubleon=any2cards&peek=off&das=on&dsa=on&charlie=on&surrender=earlyf&opt=1&btn=Generate+Strategy
	(8 decks, double on any 2 cards, Double after Split, Hit Split Aces, 6-Card charlie, No Resplits Allowed, Dealer Stands on Soft 17, Dealer does not peek, full early surrender)
	*/

    const OUTPUT_DEBUG = true;

    const SUGGESTIONS = {
        // 4 is only used in a very specific case : After 2,2 is split and you get dealt another 2. Re-splits are not allowed, and therefore you need a backup strategy (which is to always hit, based on the strategy for 2,2).
        4: {
            2: "H",
            3: "H",
            4: "H",
            5: "H",
            6: "H",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "H",
        },
        5: {
            2: "H",
            3: "H",
            4: "H",
            5: "H",
            6: "H",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "R",
        },
        6: {
            2: "H",
            3: "H",
            4: "H",
            5: "H",
            6: "H",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "R",
        },
        7: {
            2: "H",
            3: "H",
            4: "H",
            5: "H",
            6: "H",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "R",
        },
        8: {
            2: "H",
            3: "H",
            4: "H",
            5: "H",
            6: "H",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "H",
        },
        9: {
            2: "H",
            3: "D",
            4: "D",
            5: "D",
            6: "D",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "H",
        },
        10: {
            2: "D",
            3: "D",
            4: "D",
            5: "D",
            6: "D",
            7: "D",
            8: "D",
            9: "D",
            10: "H",
            A: "H",
        },
        11: {
            2: "D",
            3: "D",
            4: "D",
            5: "D",
            6: "D",
            7: "D",
            8: "D",
            9: "D",
            10: "H",
            A: "H",
        },
        12: {
            2: "H",
            3: "H",
            4: "S3",
            5: "S3",
            6: "S3",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "R",
        },
        13: {
            2: "S3",
            3: "S3",
            4: "S4",
            5: "S4",
            6: "S4",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "R",
        },
        14: {
            2: "S4",
            3: "S4",
            4: "S4",
            5: "S4",
            6: "S4",
            7: "H",
            8: "H",
            9: "H",
            10: "R",
            A: "R",
        },
        15: {
            2: "S4",
            3: "S4",
            4: "S4",
            5: "S4",
            6: "S4",
            7: "H",
            8: "H",
            9: "H",
            10: "R",
            A: "R",
        },
        16: {
            2: "S4",
            3: "S4",
            4: "S",
            5: "S",
            6: "S",
            7: "H",
            8: "H",
            9: "R",
            10: "Rs",
            A: "R",
        },
        17: {
            2: "S",
            3: "S",
            4: "S",
            5: "S",
            6: "S",
            7: "S",
            8: "S",
            9: "S4",
            10: "S4",
            A: "RS4",
        },
        18: {
            2: "S",
            3: "S",
            4: "S",
            5: "S",
            6: "S",
            7: "S",
            8: "S",
            9: "S",
            10: "S",
            A: "S",
        },
        19: {
            2: "S",
            3: "S",
            4: "S",
            5: "S",
            6: "S",
            7: "S",
            8: "S",
            9: "S",
            10: "S",
            A: "S",
        },
        20: {
            2: "S",
            3: "S",
            4: "S",
            5: "S",
            6: "S",
            7: "S",
            8: "S",
            9: "S",
            10: "S",
            A: "S",
        },
        21: {
            2: "S",
            3: "S",
            4: "S",
            5: "S",
            6: "S",
            7: "S",
            8: "S",
            9: "S",
            10: "S",
            A: "S",
        },
        "A,2": {
            2: "H",
            3: "H",
            4: "H",
            5: "H",
            6: "D",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "H",
        },
        "A,3": {
            2: "H",
            3: "H",
            4: "H",
            5: "D",
            6: "D",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "H",
        },
        "A,4": {
            2: "H",
            3: "H",
            4: "H",
            5: "D",
            6: "D",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "H",
        },
        "A,5": {
            2: "H",
            3: "H",
            4: "D",
            5: "D",
            6: "D",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "H",
        },
        "A,6": {
            2: "H",
            3: "D",
            4: "D",
            5: "D",
            6: "D",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "H",
        },
        "A,7": {
            2: "S3",
            3: "DS3",
            4: "DS3",
            5: "DS3",
            6: "DS3",
            7: "S4",
            8: "S3",
            9: "H",
            10: "H",
            A: "H",
        },
        "A,8": {
            2: "S4",
            3: "S4",
            4: "S4",
            5: "S4",
            6: "S4",
            7: "S4",
            8: "S4",
            9: "S4",
            10: "S3",
            A: "S4",
        },
        "A,9": {
            2: "S4",
            3: "S4",
            4: "S4",
            5: "S4",
            6: "S4",
            7: "S4",
            8: "S4",
            9: "S4",
            10: "S4",
            A: "S4",
        },
        "A,10": {
            2: "S4",
            3: "S4",
            4: "S4",
            5: "S4",
            6: "S4",
            7: "S4",
            8: "S4",
            9: "S4",
            10: "S4",
            A: "S4",
        },
        "2,2": {
            2: "P",
            3: "P",
            4: "P",
            5: "P",
            6: "P",
            7: "P",
            8: "H",
            9: "H",
            10: "H",
            A: "H",
        },
        "3,3": {
            2: "H",
            3: "P",
            4: "P",
            5: "P",
            6: "P",
            7: "P",
            8: "H",
            9: "H",
            10: "H",
            A: "R",
        },
        "4,4": {
            2: "H",
            3: "H",
            4: "H",
            5: "P",
            6: "P",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "H",
        },
        "5,5": {
            2: "D",
            3: "D",
            4: "D",
            5: "D",
            6: "D",
            7: "D",
            8: "D",
            9: "D",
            10: "H",
            A: "H",
        },
        "6,6": {
            2: "P",
            3: "P",
            4: "P",
            5: "P",
            6: "P",
            7: "H",
            8: "H",
            9: "H",
            10: "H",
            A: "R",
        },
        "7,7": {
            2: "P",
            3: "P",
            4: "P",
            5: "P",
            6: "P",
            7: "P",
            8: "H",
            9: "H",
            10: "R",
            A: "R",
        },
        "8,8": {
            2: "P",
            3: "P",
            4: "P",
            5: "P",
            6: "P",
            7: "P",
            8: "H",
            9: "H",
            10: "Rs",
            A: "R",
        },
        "9,9": {
            2: "P",
            3: "P",
            4: "P",
            5: "P",
            6: "P",
            7: "S",
            8: "P",
            9: "P",
            10: "S",
            A: "S",
        },
        "10,10": {
            2: "S",
            3: "S",
            4: "S",
            5: "S",
            6: "S",
            7: "S",
            8: "S",
            9: "S",
            10: "S",
            A: "S",
        },
        "A,A": {
            2: "P",
            3: "P",
            4: "P",
            5: "P",
            6: "P",
            7: "P",
            8: "P",
            9: "P",
            10: "P",
            A: "P",
        },
    };

    const formatter = new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        maximumFractionDigits: 0
    });

    let utc = new Date().toISOString();
    let yyyymmdd = utc.slice(0,4) + utc.slice(5,7) + utc.slice(8,10);

    let defaultData = {'baseBetAmount': 1000000, 'multiplier': 1, 'lastBetAmount': 0, 'totalToday': 0, 'today': yyyymmdd}
    let cards;
    let betAmountData;

    let baseData = loadFromStorage('baseData', defaultData);

    debug('load ', baseData);

    const betContainer = document.createElement("div");
    betContainer.className = "betcontainer";

    const textDiv = document.createElement("div");
    textDiv.className = "baseAmount";
    textDiv.textContent = "Base: ";

    const textDivNextBet = document.createElement("div");
    textDivNextBet.className = "nextBet";
    textDivNextBet.textContent = "Next Bet: " + formatter.format(baseData.baseBetAmount * baseData.multiplier);

    const textDivTotalWon = document.createElement("div");
    textDivTotalWon.className = "totalWon";
    textDivTotalWon.textContent = "Total Won: " + formatter.format(baseData.totalToday);

    const textDivMulti = document.createElement("div");
    textDivMulti.className = "multi";
    textDivMulti.textContent = "Current Multiplyer: " + baseData.multiplier;

    const input = document.createElement("input");
    input.type = "text";
    input.className = "baseBetInput";
    input.value = baseData.baseBetAmount;

    const updateButton = document.createElement("button");
    updateButton.className = "updateBase";
    updateButton.textContent = "Update";

    const resetButton = document.createElement("button");
    resetButton.className = "resetBase";
    resetButton.textContent = "Reset";

    textDiv.appendChild(input);
    textDiv.appendChild(updateButton);
    betContainer.appendChild(textDivNextBet);
    betContainer.appendChild(textDivTotalWon);
    betContainer.appendChild(textDivMulti);
    betContainer.appendChild(textDiv);
    betContainer.appendChild(resetButton);

    const style = document.createElement("style");
    style.textContent = `
  .nextBet, .totalWon, .multi, .baseAmount {
    font-size: 14px;
    padding: 10px;
    color: #ddd;
  }
  .baseBetInput{
    width: 100px;
    padding:2px;
  }
  .resetBase, .updateBase{
    cursor:pointer;
    border: 1px solid;
    background-color: darkgrey;
  }
`;
    document.head.appendChild(style);

    function debug(...args){
        if(OUTPUT_DEBUG){
            console.log(...args);
        }
    }

    // Save any value to localStorage (auto JSON-encodes)
    function saveToStorage(key, value) {
        try {
            const serialized = JSON.stringify(value);
            localStorage.setItem(key, serialized);
        } catch (err) {
            console.error("Failed to save to storage:", err);
        }
    }

    // Load any value from localStorage (auto JSON-decodes)
    function loadFromStorage(key, defaultValue = null) {
        try {
            const serialized = localStorage.getItem(key);
            if (serialized === null) {
                return defaultValue;
            }
            return JSON.parse(serialized);
        } catch (err) {
            console.error("Failed to load from storage:", err);
            return defaultValue;
        }
    }

    function initialiseStrategy() {
        addXHRListen2(({ detail: { page, xhr, json } }) => {
            if (page === "page") {
                let params = new URL(xhr.responseURL).searchParams;
                let sid = params.get("sid");
                if (sid === "blackjackData" && json) {
                    debug('json', json);
                    switch (json.DB.result) {
                            // case undefined:
                        case "gameStarted":
                            updateBetAmountData('gs', json.DB);
                            break;
                        case "chooseAction":
                            executeStrategy(json.DB);
                            break;
                        case "startGame":
                            showStartBet();
                            debug('sg');
                            break;
                        case "won":
                            updateBetAmountData('w', json.DB);
                            break;
                        case "wonNatural":
                            updateBetAmountData('nw', json.DB);
                            break;
                        case "lost":
                            updateBetAmountData('l', json.DB);
                            break;
                        case "dealerLost":
                            updateBetAmountData('dl', json.DB);
                            break;
                        case "draw":
                            updateBetAmountData('d', json.DB);
                            removeSuggestion();
                            break;
                        case "surrendered":
                            updateBetAmountData('s', json.DB);
                            removeSuggestion();
                            break;
                        default:
                            if (json.DB.roundNotEnded && json.DB.nextGame) {
                                executeStrategy(json.DB);
                            }
                            break;
                    }
                }
            }
        });
    }

    function getTodayNumeric() {
        const d = new Date();
        return Number(
            d.getFullYear().toString() +
            String(d.getMonth() + 1).padStart(2, "0") +
            String(d.getDate()).padStart(2, "0")
        );
    }

    function showStartBet(){
        let baseBetInput = document.find(".baseBetInput");

        if (baseBetInput) {
            baseBetInput.show();
        } else {
            document.find(".bet-action").appendChild(betContainer);
            updateButton.addEventListener("click", () => {
                updateBaseBet();
            });
            resetButton.addEventListener("click", () => {
                resetBaseBet();
            });
        }

        updatePannel();
    }

    function resetBaseBet(){
        let d = new Date();
        let utc = d.toISOString();
        let yyyymmdd = utc.slice(0,4) + utc.slice(5,7) + utc.slice(8,10);

        let baseBetInput = document.find(".baseBetInput");
        debug('resetBaseBet', baseBetInput.value);
        baseData.baseBetAmount = baseBetInput.value;
        baseData.multiplier = 1;
        baseData.totalToday = 0;
        baseData.today = yyyymmdd;
        updatePannel();
        saveToStorage('baseData', baseData);
    }

    function updatePannel(){
        let utc = new Date().toISOString();
        let yyyymmdd = utc.slice(0,4) + utc.slice(5,7) + utc.slice(8,10);

        if(baseData.today != yyyymmdd){
            resetBaseBet();
        }

        let nextBetAmount = baseData.baseBetAmount * baseData.multiplier;

        const textDivTotalWon = document.find(".totalWon");
        textDivTotalWon.textContent = "Total Won: " + formatter.format(baseData.totalToday);

        const textDivNextBet = document.find(".nextBet");
        textDivNextBet.textContent = "Next Bet: " + formatter.format(nextBetAmount);

        const input = document.find(".baseBetInput");
        input.value = baseData.baseBetAmount;

        const multi = document.find(".multi");
        multi.value = "Current Multiplyer: " + baseData.multiplier;

    }

    function updateBaseBet(){
        let baseBetInput = document.find(".baseBetInput");
        debug('updateBaseBet', baseBetInput.value);
        baseData.baseBetAmount = baseBetInput.value;
        updatePannel();
        saveToStorage('baseData', baseData);
    }

    function hideStartBet(){
        document.find(".baseBetInput").hide();
    }

    function updateBetAmountData(a, data){
        debug(a, data);
        if(a == 'gs'){
            let inputs = document.find(".bet.input-money");
            baseData.lastBetAmount = parseInt(inputs.value.replace(/[^0-9]/g, ""), 10);
            saveToStorage('baseData', baseData);
            debug('start ', baseData);
            hideStartBet();
        }

        if(a == 'w' || a == 'nw' || a == 'dl'){
            // check for double down

            baseData.multiplier = 1;
            baseData.totalToday = (parseInt(baseData.totalToday) + parseInt(baseData.lastBetAmount));
            saveToStorage('baseData', baseData);
            debug('win ', baseData);
        }

        if(a == 'l'){
            // check for double down

            baseData.multiplier = (baseData.multiplier == 1? 2: baseData.multiplier == 2? 4: 1);
            baseData.totalToday = (parseInt(baseData.totalToday) - parseInt(baseData.lastBetAmount));
            saveToStorage('baseData', baseData);
            debug('loss ', baseData);
        }

        if(a == 's'){
            baseData.multiplier = (baseData.multiplier == 1? 2: baseData.multiplier == 2? 4: 1);
            baseData.totalToday = (parseInt(baseData.totalToday) - (parseInt(baseData.lastBetAmount)/2));
            saveToStorage('baseData', baseData);
            debug('surrender ', baseData);
        }

        if(a == 'd'){
            debug('draw', baseData);
        }

    }

    function executeStrategy(data) {
        cards = { dealer: getWorth(data.dealer.hand[0]), player: [] };
        debug('executeStrategy', data);
        debug(cards);

        debug('cards.player',cards.player);
        for (let card of data.player.hand) {
            let worth = getWorth(card);
            debug('card', card, 'worth', worth);
            cards.player.push(worth);
            debug('cards.player1',cards.player);
        }

        debug('cards.player2',cards.player);

        let playerValue;
        debug('1'); // ********
        if (cards.player.length === 2) {
            debug('2'); // ********
            if (cards.player.includes("A")) {
                debug('3'); // ********
                let other = cards.player.find((worth) => worth !== "A");

                if (!other) playerValue = "A,A";
                else playerValue = `A,${other}`;
            } else if (cards.player[0] === cards.player[1]) {
                debug('4', cards); // ********
                playerValue = `${cards.player[0]},${cards.player[1]}`;
            } else {
                debug('5'); // ********
                playerValue = data.player.score;
            }
        } else {
            debug('6'); // ********
            if (cards.player.includes("A") && data.player.score !== data.player.lowestScore) {
                debug('7'); // ********
                let leftOver = cards.player.filter((card) => card !== "A").map(getWorth);
                let leftOverWorth = leftOver.totalSum() + (cards.player.length - 1 - leftOver.length);

                playerValue = `A,${leftOverWorth}`;
            } else {
                debug('7'); // ********
                playerValue = data.player.score;
            }
        }
        debug('8', playerValue); // ********
        let suggestion = getSuggestion(playerValue, data);

        let element = document.find(".blackjack-suggestion");

        if (element) element.textContent = suggestion;
        else {
            document.find(".player-cards").appendChild(document.newElement({ type: "span", class: "blackjack-suggestion", text: suggestion, style: {color: '#00a0ea', position: 'absolute', fontSize: '17px', marginTop: '15px'} }));
        }

    }

    function getWorth(card) {
        //debug('get worth card:', card);

        let symbol;
        if (typeof card === "string") {
            symbol = card.split("-").last();
            //debug('symbol',symbol, isNaN(symbol), isNaN(symbol) ? (symbol === "A" ? "A" : 10) : parseInt(symbol));
        } else symbol = card;

        //debug('symbol', symbol, parseInt(symbol));

        return isNaN(symbol) ? (symbol === "A" ? "A" : 10) : parseInt(symbol);
    }

    function getSuggestion(player, data) {
        let suggestion;
        if (player in SUGGESTIONS) {
            let dealer = cards.dealer;

            if (dealer in SUGGESTIONS[player]) {
                let action = getAction(SUGGESTIONS[player][dealer], true, data, player);

                suggestion = action in ACTIONS ? ACTIONS[action] : `no action - ${action}`;
            } else {
                suggestion = "no suggestion - dealer: " + dealer;
                //debug("no suggestion - dealer: ", dealer, player);

            }
        } else {
            suggestion = "no suggestion - " + player;
            //debug("no suggestion: ", player);
        }

        return suggestion;
    }

    function getAction(action, allowSelf, data, player) {
        if (action === "S3") return cards.player.length > 3 ? "H" : "S";
        else if (action === "S4") return cards.player.length > 4 ? "H" : "S";
        else if (action === "D" && !data.availableActions.includes("doubleDown")) return "H";
        else if (action === "DS3") return data.availableActions.includes("doubleDown") ? "D" : cards.player.length > 3 ? "H" : "S";
        else if (action === "R" && !data.availableActions.includes("surrender")) return "H";
        else if (action === "Rs") return data.availableActions.includes("surrender") ? "R" : "S";
        else if (action === "RS4") return data.availableActions.includes("surrender") ? "R" : cards.player.length > 4 ? "H" : "S";
        else if (action === "P" && !data.availableActions.includes("split")) {
            if (allowSelf) {
                let hand = player.split(",");
                if (hand[0] === hand[1]) {
                    let value;
                    if (isNaN(hand[0])) {
                        if (hand[0] === "A") {
                            return "H"; // It's not in the suggestions array, but we should always hit A,A after split
                        } else value = 20;
                    } else value = parseInt(hand[0]) * 2;

                    let alternative = getAction(SUGGESTIONS[value][cards.dealer], false);
                    if (alternative !== "P") return alternative;
                }
            }

            return "H";
        }

        return action;
    }

    function removeSuggestion() {
        let suggestion = document.find(".blackjack-suggestion");
        if (suggestion) suggestion.remove();
    }

    function addXHRListen2(callback) {
        interceptXHR2("bj-xhr2");
        window.addEventListener("bj-xhr2", callback);
    }

    function interceptXHR2(channel) {
        let oldXHROpen = window.XMLHttpRequest.prototype.open;
        let oldXHRSend = window.XMLHttpRequest.prototype.send;

        window.XMLHttpRequest.prototype.open = function (method, url) {
            let params = this.params ?? {};

            this.method = method;
            this.url = url;
            this.params = params;

            this.addEventListener("readystatechange", function () {
                if (this.readyState > 3 && this.status === 200) {
                    let page = this.responseURL.substring(this.responseURL.indexOf("torn.com/") + "torn.com/".length, this.responseURL.indexOf(".php"));

                    let json, uri;
                    if (isJsonString(this.response)) json = JSON.parse(this.response);
                    else uri = getUrlParams(this.responseURL);

                    window.dispatchEvent(
                        new CustomEvent(channel, {
                            detail: {
                                page,
                                json,
                                uri,
                                xhr: {
                                    // We used to pass the current XHR here as "...this"
                                    // but not possible due to some change in Chromium.
                                    // https://stackoverflow.com/a/53914790
                                    // https://issues.chromium.org/issues/40091619
                                    requestBody: this.requestBody,
                                    response: this.response,
                                    responseURL: this.responseURL,
                                },
                            },
                        })
                    );
                }
            });

            arguments[0] = method;
            arguments[1] = url;

            return oldXHROpen.apply(this, arguments);
        };
        window.XMLHttpRequest.prototype.send = function (body) {
            this.params = this.params ?? {};
            /*
            if (typeof xhrSendAdjustments === "object") {
                for (let key in xhrSendAdjustments) {
                    if (typeof xhrSendAdjustments[key] !== "function") continue;

                    body = xhrSendAdjustments[key]({ ...this }, body);
                }
            }
*/
            this.requestBody = body;

            arguments[0] = body;

            return oldXHRSend.apply(this, arguments);
        };

        //
	 //* JavaScript Get URL Parameter (https://www.kevinleary.net/javascript-get-url-parameters/)
	 //
        function getUrlParams(url, prop) {
            if (!url) url = location.href;

            let search = decodeURIComponent(url.slice(url.indexOf("?") + 1));
            let definitions = search.split("&");

            let params = {};
            definitions.forEach((val) => {
                let parts = val.split("=", 2);

                params[parts[0]] = parts[1];
            });

            return prop && prop in params ? params[prop] : params;
        }

        // Global functions
        function isJsonString(str) {
            if (!str || str === "") return false;

            try {
                JSON.parse(str);
            } catch (e) {
                return false;
            }
            return true;
        }
    }

    // noinspection JSUnusedGlobalSymbols
    function getParams(body) {
        let params = {};

        for (let param of body.split("&")) {
            let split = param.split("=");

            params[split[0]] = split[1];
        }

        return params;
    }

    // noinspection JSUnusedGlobalSymbols
    function paramsToBody(params) {
        let _params = [];

        for (let key in params) {
            _params.push(key + "=" + params[key]);
        }

        return _params.join("&");
    }

    function _find(element, selector, options = {}) {
        options = {
            text: false,
            ...options,
        };

        if (options.text) {
            for (let element of document.querySelectorAll(selector)) {
                if (element.textContent === options.text) {
                    return element;
                }
            }
        }

        if (selector.includes("=") && !selector.includes("[")) {
            let key = selector.split("=")[0];
            let value = selector.split("=")[1];

            for (let element of document.querySelectorAll(key)) {
                if (element.textContent.trim() === value.trim()) {
                    return element;
                }
            }

            try {
                element.querySelector(selector);
            } catch (err) {
                return undefined;
            }
        }
        return element.querySelector(selector);
    }

    Object.defineProperty(Document.prototype, "find", {
        value(selector, options = {}) {
            return _find(this, selector, options);
        },
        enumerable: false,
    });

    Object.defineProperty(Array.prototype, "last", {
        value() {
            return this[this.length - 1];
        },
        enumerable: false,
    });

    Object.defineProperty(Document.prototype, "newElement", {
        value(options = {}) {
            if (typeof options === "string") {
                return this.createElement(options);
            } else if (typeof options === "object") {
                options = {
                    type: "div",
                    id: false,
                    class: false,
                    text: false,
                    html: false,
                    value: false,
                    href: false,
                    children: [],
                    attributes: {},
                    events: {},
                    style: {},
                    dataset: {},
                    ...options,
                };

                let newElement = this.createElement(options.type);

                if (options.id) newElement.id = options.id;
                if (options.class) {
                    if (Array.isArray(options.class)) newElement.setClass(...options.class.filter((name) => !!name));
                    else newElement.setClass(options.class.trim());
                }
                if (options.text !== false) newElement.textContent = options.text;
                if (options.html) newElement.innerHTML = options.html;
                if (options.value) {
                    if (typeof options.value === "function") newElement.value = options.value();
                    else newElement.value = options.value;
                }
                if (options.href) newElement.href = options.href;

                for (let child of options.children.filter((child) => !!child) || []) {
                    if (typeof child === "string") {
                        newElement.appendChild(document.createTextNode(child));
                    } else {
                        newElement.appendChild(child);
                    }
                }

                if (options.attributes) {
                    let attributes = options.attributes;
                    if (typeof attributes === "function") attributes = attributes();

                    for (let attribute in attributes) newElement.setAttribute(attribute, attributes[attribute]);
                }
                for (let event in options.events) newElement.addEventListener(event, options.events[event]);

                for (let key in options.style) newElement.style[key] = options.style[key];
                for (let key in options.dataset) {
                    if (typeof options.dataset[key] === "object") newElement.dataset[key] = JSON.stringify(options.dataset[key]);
                    else newElement.dataset[key] = options.dataset[key];
                }

                return newElement;
            }
        },
        enumerable: false,
    });

    Object.defineProperty(Document.prototype, "setClass", {
        value(...classNames) {
            this.setAttribute("class", classNames.join(" "));
        },
        enumerable: false,
    });

    Object.defineProperty(Element.prototype, "setClass", {
        value(...classNames) {
            this.setAttribute("class", classNames.join(" "));
        },
        enumerable: false,
    });

    Object.defineProperty(Element.prototype, "hide", {
        value() {
            this.setAttribute("style", "display: none;");
        },
        enumerable: false,
    });

    Object.defineProperty(Element.prototype, "show", {
        value() {
            this.setAttribute("style", "display: block;");
        },
        enumerable: false,
    });

    initialiseStrategy();
})();