BGA Pythia - 7 Wonders game helper

Visual aid that extends BGA game interface with useful information

// ==UserScript==
// @name         BGA Pythia - 7 Wonders game helper
// @description  Visual aid that extends BGA game interface with useful information
// @namespace    https://github.com/dpavliuchkov/bga-pythia
// @author       https://github.com/dpavliuchkov
// @version      1.3.3
// @license      MIT
// @include      *boardgamearena.com/*
// @grant        none
// ==/UserScript==
//
// On boardgamearena.com, you can play an exciting board game of 7 wonders.
// However, it is hard to remember which cards each player has. Pythia has
// godlike powers and will share this information with you. It will also
// display total player's score based on the current shields situation.
// And it will mark leader and runner up players and their boards. And
// Pythia will calculate how much points and coins some cards are worth to you.
// Works with Tampermonkey only.
// ==/UserScript==

// System variables - don't edit
const Enable_Logging = false;
const Is_Inside_Game = /\?table=[0-9]*/.test(window.location.href);
const Cards_Image = "https://x.boardgamearena.net/data/themereleases/current/games/sevenwonders/200914-1526/img/cards.jpg";
const Cards_Image_V2 = "https://x.boardgamearena.net/data/themereleases/current/games/sevenwonders/200914-1526/img/cards_v2.jpg";
const BGA_Player_Board_Id_Prefix = "player_board_wrap_";
const BGA_Player_Score_Id_Prefix = "player_score_";
const Card_Worth_Id_Prefix = "pythia_card_worth_container_";
const Discard_Card_Worth_Id_Prefix = "pythia_discard_card_worth_container_";
const Card_Worth_Class = "pythia_card_worth";
const Card_Worth_Coins_Class = "pythia_card_coins_worth";
const Player_Cards_Id_Prefix = "pythia_cards_wrap_";
const Player_Hand_Card_Id_Prefix = "pythia_hand_card_";
const Player_Score_Id_Prefix = "pythia_score_";
const Player_Military_Power_Id_Prefix = "pythia_military_power_";
const Player_War_Score_Id_Prefix = "pythia_player_war_score_";
const Player_Cards_Div_Class = "pythia_cards_container";
const Player_Score_Span_Class = "pythia_score";
const Player_Leader_Class = "pythia_leader";
const Player_Runnerup_Class = "pythia_runnerup";
const War_Points_Per_Age = {
    "1": 1,
    "2": 3,
    "3": 5
};
const Victory_Points_Image = {
    "-1": "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/minus%201%20point.png?raw=true",
    "-2": "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/minus%202%20points.png?raw=true",
    0: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/0%20points.png?raw=true",
    1: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/1%20point.png?raw=true",
    2: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/2%20points.png?raw=true",
    3: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/3%20points.png?raw=true",
    4: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/4%20points.png?raw=true",
    5: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/5%20points.png?raw=true",
    6: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/6%20points.png?raw=true",
    7: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/7%20points.png?raw=true",
    8: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/8%20points.png?raw=true",
    9: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/9%20points.png?raw=true",
    10: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/10%20points.png?raw=true",
    11: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/11%20points.png?raw=true",
    12: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/12%20points.png?raw=true",
    13: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/13%20points.png?raw=true",
    14: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/14%20points.png?raw=true",
};
const Coins_Image = {
    0: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/0%20coins.png?raw=true",
    1: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/1%20coin.png?raw=true",
    2: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/2%20coins.png?raw=true",
    3: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/3%20coins.png?raw=true",
    4: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/4%20coins.png?raw=true",
    5: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/5%20coins.png?raw=true",
    6: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/6%20coins.png?raw=true",
    7: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/7%20coins.png?raw=true",
    8: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/8%20coins.png?raw=true",
    9: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/9%20coins.png?raw=true",
    10: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/10%20coins.png?raw=true",
    11: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/11%20coins.png?raw=true",
    12: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/12%20coins.png?raw=true",
    13: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/13%20coins.png?raw=true",
    14: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/14%20coins.png?raw=true",
};
const Military_Power_Icon = "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/military-power-icon.png?raw=true";
const HD_Boards = "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/boards_hd.jpg?raw=true";
const HD_Cards = "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/cards_hd.jpg?raw=true&version=1";

// Main Pythia object
var pythia = {
    isStarted: false,
    isFinished: false,
    dojo: null,
    game: null,
    edition: null,
    isNewEdition: null,
    mainPlayer: null,
    currentAge: 1,
    playersCount: 0,
    players: [],

    // Init Pythia
    init: function() {
        this.isStarted = true;
        // Check if the site was loaded correctly
        if (!window.parent || !window.parent.dojo || !window.parent.gameui.gamedatas ||
            !window.parent.gameui.gamedatas.playerorder || !window.parent.gameui.gamedatas.playerorder[0] ||
            !window.parent.gameui.gamedatas.card_types || !window.parent.gameui.gamedatas.wonders) {
            return;
        }
        this.dojo = window.parent.dojo;
        this.game = window.parent.gameui.gamedatas;
        this.edition = parseInt(this.game.game_edition);
        this.isNewEdition = this.edition == 1;

        var playerOrder = this.game.playerorder;
        this.playersCount = playerOrder.length;
        this.mainPlayer = playerOrder[0];
        // local storage stores value as strings, so we need to parse "false" and "true" to get boolean
        this.settings = {
            "enableWarScores": localStorage.getItem("pythia-seetings-warscores") === null ?
                true : String(localStorage.getItem("pythia-seetings-warscores")) == "true",
            "enableRichBoards": localStorage.getItem("pythia-seetings-richboards") === null ?
                true : String(localStorage.getItem("pythia-seetings-richboards")) == "true",
            "enableCardPoints": localStorage.getItem("pythia-seetings-cardpoints") === null ?
                true : String(localStorage.getItem("pythia-seetings-cardpoints")) == "true",
            "enableHD": localStorage.getItem("pythia-seetings-hd") === null ?
                true : String(localStorage.getItem("pythia-seetings-hd")) == "true",
            "enableFullwidth": localStorage.getItem("pythia-seetings-fullwidth") === null ?
                false : String(localStorage.getItem("pythia-seetings-fullwidth")) == "true",
            "showMenu": localStorage.getItem("pythia-seetings-showmenu") === null ?
                true : String(localStorage.getItem("pythia-seetings-showmenu")) == "true",
        };

        for (var i = 0; i < this.playersCount; i++) {
            var playerId = playerOrder[i];
            this.players[playerId] = {
                hand: {},
                coins: 3,
                shields: 0,
                defeats: 0,
                bgaScore: 1,
                warScore: 0,
                wonder: 0,
                wonderStages: 0,
                maxWonderStages: 0,
                playedTypes: {
                    "raw": 0,
                    "man": 0,
                    "com": 0,
                    "mil": 0,
                    "civ": 0,
                    "sci": 0,
                    "gui": 0,
                },
                playedCards: {
                    "lighthouse": false,
                    "haven": false,
                    "ludus": false,
                    "chamberOfCommerce": false,
                    "shipownersGuild": false,
                },
                science: {
                    "gears": 0, // 1 in BGA
                    "tablets": 0, // 2 in BGA
                    "compasses": 0, // 3 in BGA
                    "jokers": 0, // ? in BGA
                }
            };

            // Identify who sits to the left and to the right
            if (playerId == this.mainPlayer) {
                this.players[playerId].left = playerOrder[this.playersCount - 1];
            } else {
                this.players[playerId].left = playerOrder[i - 1];
            }
            if (playerId == playerOrder[this.playersCount - 1]) {
                this.players[playerId].right = this.mainPlayer;
            } else {
                this.players[playerId].right = playerOrder[i + 1];
            }

            this.renderPythiaContainers(playerId);
        }

        this.renderPythiaMenu();
        this.setStyles();

        // Configure Pythia according to settings
        this.togglePythiaSettingPlayerCardsDisplay(false);
        this.togglePythiaSettingWarScoresDisplay(this.settings.enableWarScores);
        this.togglePythiaSettingRichBoardsDisplay(this.settings.enableRichBoards);
        this.togglePythiaSettingCardPointsDisplay(this.settings.enableCardPoints);
        this.togglePythiaSettingHDDisplay(this.settings.enableHD);
        this.togglePythiaSettingFullwidthDisplay(this.settings.enableFullwidth);

        // Connect event handlers to follow game progress
        this.dojo.subscribe("newHand", this, "recordHand");
        this.dojo.subscribe("cardsPlayed", this, "recordTurn");
        this.dojo.subscribe("coinDelta", this, "recordCoins");
        this.dojo.subscribe("discard", this, "recordDiscard");
        this.dojo.subscribe("newWonders", this, "recordWonderChoice");
        this.dojo.subscribe("wonderBuild", this, "recordWonderStage");
        this.dojo.subscribe("updateScore", this, "recordScoreUpdate");
        this.dojo.subscribe("warVictory", this, "recordWarResults");
        this.dojo.subscribe("newAge", this, "changeAge");

        if (Enable_Logging) console.log("PYTHIA: My eyes can see everything!");
        return this;
    },

    // Record which wonder each player has chosen
    recordWonderChoice: function(data) {
        if (Enable_Logging) console.log("PYTHIA: wonders chosen - I got", data);

        // Input check
        if (!data || !data.args || !data.args.wonders || !this.game || !this.game.wonders) {
            return;
        }

        const wonders = Object.keys(data.args.wonders);
        for (const playerId of wonders) {
            const wonderId = data.args.wonders[playerId]
            this.players[playerId].wonder = wonderId;
            this.players[playerId].maxWonderStages = Object.keys(this.game.wonders[wonderId].stages).length;
        }
    },

    // Check what came to main player in the new hand
    recordHand: function(data) {
        if (Enable_Logging) console.log("PYTHIA: new hand - I got", data);

        // Input check
        if (!data || !data.args || !data.args.cards) {
            return;
        }

        // Rotate old hands and render cards
        if (!this.isFirstTurn()) {
            this.passCards();
            this.renderPlayerCards();
            this.renderPlayerCardTooltips();
        }
        // Save new hand to main player
        this.players[this.mainPlayer].hand = data.args.cards;

        // Get card worth in victory points and coins and render it
        for (var cardId in data.args.cards) {
            const playedCard = data.args.cards[cardId];
            const cardWorth = this.calculateCardWorth(this.mainPlayer, playedCard.type);
            this.renderCardPoints(cardId, false, cardWorth.points, cardWorth.coins);
        }
    },

    // Process all cards played by all players
    recordTurn: function(data) {
        if (Enable_Logging) console.log("PYTHIA: cards played - I got", data);

        // Input check
        if (!data || !data.args || !data.args.cards) {
            return;
        }

        var warPlayed = false;

        // Cycle all played cards
        for (var cardId in data.args.cards) {
            const playedCard = data.args.cards[cardId];
            const originalCard = this.game.card_types[playedCard.type];
            const playerId = playedCard.location_arg;
            if (!originalCard.category) return; // Input check

            const cardCategory = originalCard.category;

            // Track if played card was military
            if (originalCard.shield) {
                warPlayed = true;
                this.players[playerId].shields += originalCard.shield;
                this.renderMilitaryPower(playerId);
            }

            // Update played scientific symbols
            if (originalCard.science) {
                this.increaseScienceCounter(playerId, originalCard.science);
            }

            // Update played card types array of the player
            this.players[playerId].playedTypes[cardCategory] += 1;

            // Track cards that we need for points worth feature
            this.increaseCardCounter(playerId, playedCard.type);

            // Delete played card
            if (isObjectEmpty(this.players[playerId].hand)) {
                continue;
            }
            delete this.players[playerId].hand[cardId];
        }
        if (warPlayed) {
            this.calculateWarScores();
        }
    },

    // If there was a trade, update how many coins each player has
    recordCoins: function(data) {
        if (Enable_Logging) console.log("PYTHIA: coins changed - I got", data);

        // Input check
        if (!data || !data.args || !data.args.coinddelta) {
            return;
        }

        this.players[data.args.player_id].coins += data.args.coinddelta;
    },

    // If main player discarded - we know what card it was
    recordDiscard: function(data) {
        if (Enable_Logging) console.log("PYTHIA: card discarded - I got", data);

        // Input check
        if (!data || !data.args || !data.args.card_id || !!data.channelorig) {
            return;
        }

        var player = data.channelorig.substring(9);
        delete this.players[player].hand[data.args.card_id];
    },

    // Record when a wonder stage was played
    recordWonderStage: function(data) {
        if (Enable_Logging) console.log("PYTHIA: wonder built - I got", data);

        // Input check
        if (!data || !data.args || !data.args.step || !data.args.player_id) {
            return;
        }

        const playerId = data.args.player_id;
        const stage = data.args.step;
        var wonderId = this.players[playerId].wonder;

        // We might have corrupted data, refresh it from the game HTML
        if (wonderId == 0) {
            const board = this.dojo.byId("player_board_wonder_" + playerId);
            switch (board.style.backgroundPositionY) {
                case "0%":
                    // Giza A
                    wonderId = 1;
                    break;

                case "7.69231%":
                    // Baby A
                    wonderId = 2;
                    break;

                case "15.3846%":
                    // Oly A
                    wonderId = 3;
                    break;

                case "23.0769%":
                    // Rho A
                    wonderId = 4;
                    break;

                case "30.7692%":
                    // Eph A
                    wonderId = 5;
                    break;

                case "38.4615%":
                    // Alex A
                    wonderId = 6;
                    break;

                case "46.1538%":
                    // Hali A
                    wonderId = 7;
                    break;

                case "53.8462%":
                    // Giza B
                    wonderId = 8;
                    break;

                case "61.5385%":
                    // Baby B
                    wonderId = 9;
                    break;

                case "69.2308%":
                    // Oly B
                    wonderId = 10;
                    break;

                case "76.9231%":
                    // Rho B
                    wonderId = 11;
                    break;

                case "84.6154%":
                    // Eph B
                    wonderId = 12;
                    break;

                case "92.3077%":
                    // Alex B
                    wonderId = 13;
                    break;

                case "100%":
                    // Hali B
                    wonderId = 14;
                    break;
            }

            this.players[playerId].wonder = wonderId;
        }

        this.players[playerId].wonderStages += 1; // increase a counter of built wonder stages

        // Game correctness check
        if (!this.game.wonders[wonderId] || !this.game.wonders[wonderId].stages ||
            !this.game.wonders[wonderId].stages[stage]) {
            return;
        }

        // If Rhodos built a stage - it could have shields
        if (this.game.wonders[wonderId].stages[stage].shield) {
            this.players[playerId].shields += this.game.wonders[wonderId].stages[stage].shield;
            this.calculateWarScores();
            this.renderMilitaryPower(playerId);
        }

        // If Babylon built a stage - it could have science
        if (this.game.wonders[wonderId].stages[stage].science) {
            this.increaseScienceCounter(playerId, this.game.wonders[wonderId].stages[stage].science);
        }

        // If Hali built a stage - it could have discard play
        if (this.game.wonders[wonderId].stages[stage].pickDiscarded) {
            this.calculateDiscardWorth(this);
        }
    },

    // Update internal scores as well
    recordScoreUpdate: function(data) {
        if (Enable_Logging) console.log("PYTHIA: scores updated - I got", data);

        // Input check
        if (!data || !data.args || !data.args.scores) {
            return;
        }

        const scores = Object.keys(data.args.scores);
        for (const playerId of scores) {
            this.players[playerId].bgaScore = data.args.scores[playerId];
            this.renderPlayerScore(playerId);
        }

        // Update leader & runnerup positions
        this.renderLeaderRunnerup();
    },

    // Process war results
    recordWarResults: function(data) {
        if (Enable_Logging) console.log("PYTHIA: war battle happened - I got", data);

        // Input check
        if (!data || !data.args || !data.args.points || !data.args.neighbour_id) {
            return;
        }

        // Save defeat tokens
        if (data.args.points > 0) {
            this.players[data.args.neighbour_id].defeats += 1;
        }

        // If this is the last war - do cleanup
        if (this.currentAge == 3) {
            this.finishGame();
        }
    },

    // Calculate additional score from shields
    calculateWarScores: function() {
        var currentPlayerId = this.mainPlayer;
        var i = 0;
        while (i < this.playersCount) {
            var thisPlayer = this.players[currentPlayerId];
            thisPlayer.warScore = 0;

            // Check battles with right neighbour
            var rightPlayer = this.players[thisPlayer.right];
            if (thisPlayer.shields > rightPlayer.shields) {
                this.increaseWarScore(currentPlayerId, this.currentAge);
            } else if (thisPlayer.shields < rightPlayer.shields) {
                this.decreaseWarScore(currentPlayerId, this.currentAge);
            }

            // Check battles with left neighbour
            var leftPlayer = this.players[thisPlayer.left];
            if (thisPlayer.shields > leftPlayer.shields) {
                this.increaseWarScore(currentPlayerId, this.currentAge);
            } else if (thisPlayer.shields < leftPlayer.shields) {
                this.decreaseWarScore(currentPlayerId, this.currentAge);
            }

            currentPlayerId = thisPlayer.right;
            i++;
        }
    },

    calculateDiscardWorth: function(that, counter = 1) {
        const discardWrapper = that.dojo.byId('discarded_wrap');

        // Check that we can see discard div, else wait
        if (discardWrapper.style.display == "block") {
            const discarded = that.dojo.query("#discarded div");
            for (var i in discarded) {
                const card = discarded[i];
                if (!card.style) {
                    continue;
                }

                // Calculate card position - the only way to find which card is actually in discard
                var posX, posY;
                if (that.isNewEdition) {
                    posX = -255 * parseInt(card.style.backgroundPositionX) / 100;
                    posY = -393 * parseInt(card.style.backgroundPositionY) / 100;
                } else {
                    posX = -parseInt(card.style.width) * parseInt(card.style.backgroundPositionX) / 100;
                    posY = -parseInt(card.style.height) * parseInt(card.style.backgroundPositionY) / 100;
                }

                var cardType = null;
                for (var j in that.game.card_types) {
                    const originalCard = that.game.card_types[j];
                    if (originalCard.backx == posX && originalCard.backy == posY) {
                        cardType = j;
                        break;
                    }
                }

                if (cardType) {
                    const cardWorth = that.calculateCardWorth(that.mainPlayer, cardType);
                    that.renderCardPoints(card.id.substr(15), true, cardWorth.points, cardWorth.coins);
                }
            }
        } else {
            if (counter > 10) {
                clearTimeout();
                return;
            }

            setTimeout(that.calculateDiscardWorth, 2000, that, counter + 1);
        }
    },

    // Get how many coins and victory points this card will bring to this player
    calculateCardWorth: function(playerId, cardType) {
        const leftPlayerId = this.players[playerId].left;
        const rightPlayerId = this.players[playerId].right;

        var worth = {
            points: null,
            coins: null,
        };

        switch (parseInt(cardType)) {
            // Raw materials
            case 1:
            case 2:
            case 3:
            case 4:
            case 5:
            case 6:
            case 7:
            case 8:
            case 9:
            case 10:
            case 28:
            case 28:
            case 30:
            case 31:
                if (this.players[playerId].playedCards.haven) {
                    worth.points += 1;
                }
                if (this.players[playerId].playedCards.shipownersGuild) {
                    worth.points += 1;
                }
                break;


            // Manufactured goods
            case 11:
            case 12:
            case 13:
            case 32:
            case 33:
            case 34:
            case 39:
            case 40:
                if (this.players[playerId].playedCards.shipownersGuild) {
                    worth.points += 1;
                }
                if (this.players[playerId].playedCards.chamberOfCommerce) {
                    worth.points += 2;
                }
                break;

            // Commercial cards
            case 18:
            case 19:
            case 20:
            case 21:
                if (this.players[playerId].playedCards.lighthouse) {
                    worth.points += 1;
                }
                break;

            // Military cards
            case 22:
            case 23:
            case 24:
            case 43:
            case 44:
            case 45:
            case 46:
            case 70:
            case 71:
            case 72:
            case 73:
            case 80:
                if (!this.game.card_types[cardType] || !this.game.card_types[cardType].shield) {
                    break;
                }
                worth.points = this.calculateMilitaryCardWorth(playerId, parseInt(this.game.card_types[cardType].shield));
                if (this.players[playerId].playedCards.ludus) {
                    worth.points += 1;
                }
                break;

            // Scientific symbols
            case 25:
            case 26:
            case 27:
            case 47:
            case 48:
            case 49:
            case 50:
            case 58:
            case 74:
            case 75:
            case 76:
            case 77:
            case 78:
                if (!this.game.card_types[cardType] || !this.game.card_types[cardType].science) {
                    break;
                }
                worth.points = this.calculateScienceCardWorth(playerId, this.game.card_types[cardType].science);
                break;

            // Age 2 commerce
            case 41: // Vineyard - coins for brown cards
                worth.coins = this.players[leftPlayerId].playedTypes["raw"] + this.players[rightPlayerId].playedTypes["raw"] +
                    this.players[playerId].playedTypes["raw"];
                if (this.players[playerId].playedCards.lighthouse) {
                    worth.points += 1;
                }
                break;

            case 42: // Bazaar - coins for grey cards
                worth.coins = (this.players[leftPlayerId].playedTypes["man"] + this.players[rightPlayerId].playedTypes["man"] +
                    this.players[playerId].playedTypes["man"]) * 2;
                if (this.players[playerId].playedCards.lighthouse) {
                    worth.points += 1;
                }
                break;

            // Age 3 guilds
            case 51: // Workers guild - brown cards
                worth.points = this.players[leftPlayerId].playedTypes["raw"] + this.players[rightPlayerId].playedTypes["raw"];
                if (this.players[playerId].playedCards.shipownersGuild) {
                    worth.points += 1;
                }
                break;
            case 52: // Craftsmens guild - grey cards
                worth.points = 2 * (this.players[leftPlayerId].playedTypes["man"] + this.players[rightPlayerId].playedTypes["man"]);
                if (this.players[playerId].playedCards.shipownersGuild) {
                    worth.points += 1;
                }
                break;
            case 53: // Traders guild - yellow cards
                worth.points = this.players[leftPlayerId].playedTypes["com"] + this.players[rightPlayerId].playedTypes["com"];
                if (this.players[playerId].playedCards.shipownersGuild) {
                    worth.points += 1;
                }
                break;
            case 54: // Philosopehrs guild - yellow cards
                worth.points = this.players[leftPlayerId].playedTypes["sci"] + this.players[rightPlayerId].playedTypes["sci"];
                if (this.players[playerId].playedCards.shipownersGuild) {
                    worth.points += 1;
                }
                break;
            case 55: // Spies guild - red cards
                worth.points = this.players[leftPlayerId].playedTypes["mil"] + this.players[rightPlayerId].playedTypes["mil"];
                if (this.players[playerId].playedCards.shipownersGuild) {
                    worth.points += 1;
                }
                break;
            case 56:
                if (this.isNewEdition) {
                    // Decorators guild - if all stages are built then 7 points
                    worth.points = this.players[playerId].wonderStages == this.players[playerId].maxWonderStages ? 7 : 0;
                } else {
                    // Strategist guild - defeat tokens
                    worth.points = this.players[leftPlayerId].defeats + this.players[rightPlayerId].defeats;
                }
                if (this.players[playerId].playedCards.shipownersGuild) {
                    worth.points += 1;
                }
                break;
            case 57: // Shipowners guild - own brown grey purple cards
                worth.points = this.players[playerId].playedTypes["raw"] + this.players[playerId].playedTypes["man"] +
                    this.players[playerId].playedTypes["gui"] + 1;
                break;
            case 59: // Magistrates guild - blue cards
                worth.points = this.players[leftPlayerId].playedTypes["civ"] + this.players[rightPlayerId].playedTypes["civ"];
                if (this.players[playerId].playedCards.shipownersGuild) {
                    worth.points += 1;
                }
                break;
            case 60: // Builders guild - wonder stages]
                worth.points = this.players[playerId].wonderStages + this.players[leftPlayerId].wonderStages +
                    this.players[rightPlayerId].wonderStages;
                if (this.players[playerId].playedCards.shipownersGuild) {
                    worth.points += 1;
                }
                break;

            // Age 3 commerce
            case 66: // Haven - coins and points for own brown cards
                worth.coins = this.players[playerId].playedTypes["raw"];
                worth.points = this.players[playerId].playedTypes["raw"];
                if (this.players[playerId].playedCards.lighthouse) {
                    worth.points += 1;
                }
                break;
            case 67: // Lighthouse - coins and points for own yellow cards
                worth.coins = this.players[playerId].playedTypes["com"] + 1;
                worth.points = this.players[playerId].playedTypes["com"] + 1;
                break;
            case 68: // Chamber of commerce - coins and points for own grey cards
                worth.coins = this.players[playerId].playedTypes["man"] * 2;
                worth.points = this.players[playerId].playedTypes["man"] * 2;
                if (this.players[playerId].playedCards.lighthouse) {
                    worth.points += 1;
                }
                break;
            case 69: // Arena - coins and points for own wonder stages
                worth.coins = this.players[playerId].wonderStages * 3;
                worth.points = this.players[playerId].wonderStages;
                if (this.players[playerId].playedCards.lighthouse) {
                    worth.points += 1;
                }
                break
            case 79: // Ludus - coins and points for own military stages
                worth.coins = this.players[playerId].playedTypes["mil"] * 3;
                worth.points = this.players[playerId].playedTypes["mil"];
                if (this.players[playerId].playedCards.lighthouse) {
                    worth.points += 1;
                }
                break;
        }

        return worth;
    },

    // How will war score change if a player gets extra shields
    calculateMilitaryCardWorth: function(playerId, extraShields) {
        // Input check
        if (!playerId || !this.players[playerId]) {
            return 0;
        }

        var thisPlayer = this.players[playerId];
        var newWarScore = 0;

        // Check battles with right neighbour
        var rightPlayer = this.players[thisPlayer.right];
        if ((thisPlayer.shields + extraShields) > rightPlayer.shields) {
            newWarScore += War_Points_Per_Age[this.currentAge];
        } else if ((thisPlayer.shields + extraShields) < rightPlayer.shields) {
            newWarScore -= 1;
        }

        // Check battles with left neighbour
        var leftPlayer = this.players[thisPlayer.left];
        if ((thisPlayer.shields + extraShields) > leftPlayer.shields) {
            newWarScore += War_Points_Per_Age[this.currentAge];
        } else if ((thisPlayer.shields + extraShields) < leftPlayer.shields) {
            newWarScore -= 1;
        }

        return newWarScore - thisPlayer.warScore;
    },

    // How many points will this science card bring to a player?
    calculateScienceCardWorth: function(playerId, newSymbol) {
        // Input check
        if (!playerId || !this.players[playerId] || !this.players[playerId].science) {
            return;
        }

        const playerScience = this.players[playerId].science;
        const sciencePointsNow = this.calculateSciencePoints(playerScience.gears, playerScience.tablets,
            playerScience.compasses, playerScience.jokers);
        var sciencePointsAfter = null;

        switch (newSymbol) {
            case 1:
                sciencePointsAfter = this.calculateSciencePoints(playerScience.gears + 1,
                    playerScience.tablets, playerScience.compasses, playerScience.jokers);
                break;

            case 2:
                sciencePointsAfter = this.calculateSciencePoints(playerScience.gears,
                    playerScience.tablets + 1, playerScience.compasses, playerScience.jokers);
                break;

            case 3:
                sciencePointsAfter = this.calculateSciencePoints(playerScience.gears,
                    playerScience.tablets, playerScience.compasses + 1, playerScience.jokers);
                break;

            case "?":
                sciencePointsAfter = this.calculateSciencePoints(playerScience.gears,
                    playerScience.tablets, playerScience.compasses, playerScience.jokers + 1);
                break;

            default:
                break;
        }

        return sciencePointsAfter - sciencePointsNow;
    },

    // How many points will a science set bring?
    calculateSciencePoints: function(gears, tablets, compasses, jokers) {
        // Joker can be any symbol, calculate each option with recursion if we have them
        if (jokers > 0) {
            const pointsWithJokerGear = this.calculateSciencePoints(gears + 1, tablets, compasses, jokers - 1);
            const pointsWithJokerTablet = this.calculateSciencePoints(gears, tablets + 1, compasses, jokers - 1);
            const pointsWithJokerCompass = this.calculateSciencePoints(gears, tablets, compasses + 1, jokers - 1);

            return Math.max(pointsWithJokerGear, pointsWithJokerTablet, pointsWithJokerCompass);
        } else {
            // No jokers - calculate according to the rules
            var points = gears * gears + tablets * tablets + compasses * compasses; // individual symbols
            points += 7 * Math.min(gears, tablets, compasses); // set of 3

            return points;
        }
    },

    // Cleanup things between ages
    changeAge: function(data) {
        if (Enable_Logging) console.log("PYTHIA: new age - I got", data);

        this.currentAge += 1;

        // Recalculate war scores for the new age
        this.calculateWarScores();

        const keys = Object.keys(this.players);
        for (const playerId of keys) {
            // Clean player hands and update total scores
            this.players[playerId].hand = {};
            this.renderPlayerScore(playerId);
            this.renderLeaderRunnerup();
        }

        // Clean rendered cards from previous age
        this.dojo.query("." + Player_Cards_Div_Class).forEach(this.dojo.empty);
    },

    // Cleanup Pythia when the game is done
    finishGame: function() {
        this.isFinished = true;
        this.togglePythiaSettingRichBoardsDisplay(false);
        this.togglePythiaSettingWarScoresDisplay(false);
        this.togglePythiaSettingPlayerCardsDisplay(false);
    },

    // Add war scores based on the age
    increaseWarScore: function(playerId, age) {
        this.players[playerId].warScore += War_Points_Per_Age[age];
    },
    // Decrase war scores
    decreaseWarScore: function(playerId, age) {
        this.players[playerId].warScore -= 1;
    },

    // Add a counter to played science symbols
    increaseScienceCounter: function(player, symbol) {
        switch (symbol) {
            case 1:
                this.players[player].science.gears += 1;
                break;
            case 2:
                this.players[player].science.tablets += 1;
                break;
            case 3:
                this.players[player].science.compasses += 1;
                break;
            case "?":
                this.players[player].science.jokers += 1;
                break;
            default:
                break;
        }
    },

    // Track if certain card was player by hte player, needed for Card Worth function
    increaseCardCounter: function(playerId, cardType) {
        switch (parseInt(cardType)) {
            case 57: // Shipowners guild - own brown grey purple cards
                this.players[playerId].playedCards.shipownersGuild = true;
                break;
            case 66: // Haven - coins and points for own brown cards
                this.players[playerId].playedCards.haven = true;
                break;
            case 67: // Lighthouse - coins and points for own yellow cards
                this.players[playerId].playedCards.lighthouse = true;
                break;
            case 68: // Chamber of commerce - coins and points for own grey cards
                this.players[playerId].playedCards.chamberOfCommerce = true;
                break;
            case 79: // Ludus - coins and points for own military stages
                this.players[playerId].playedCards.ludus = true;
                break;
        }
    },

    // Move cards unplayed cards between players
    passCards: function() {
        // This should be counter to age direction, because
        // Pythia always passes starting from the last player
        var direction = this.currentAge == 2 ? "right" : "left";
        var currentPlayerId = this.mainPlayer;
        var i = 0;
        while (i < this.playersCount) {
            var neighborId = this.players[currentPlayerId][direction];
            this.players[neighborId].hand = this.players[this.players[neighborId][direction]].hand;
            currentPlayerId = neighborId;
            i++;
        }
    },

    // Render player containers
    renderPythiaContainers: function(playerId) {
        // Insert war score container in scores table
        if (!this.dojo.byId(Player_Score_Id_Prefix + playerId)) {
            this.dojo.place(
                "<span id='" + Player_Score_Id_Prefix + playerId + "'" +
                "class='player_score_value " + Player_Score_Span_Class + "'> (1)</span>",
                BGA_Player_Score_Id_Prefix + playerId,
                "after");
        }

        // Insert military power container on player board
        if (!this.dojo.byId(Player_Military_Power_Id_Prefix + playerId)) {
            const refNode = this.dojo.query("#" + BGA_Player_Board_Id_Prefix + playerId + " .sw_coins");
            if (refNode && refNode[0]) {
                this.dojo.place(
                    "<div id='" + Player_Military_Power_Id_Prefix + playerId + "' class='pythia_player_military_power'>" +
                    "<img src='" + Military_Power_Icon + "'/><span>0</span></div>",
                    refNode[0],
                    "last");
            }
        }

        // Insert war score container on player board
        if (!this.dojo.byId(Player_War_Score_Id_Prefix + playerId)) {
            const refNode = this.dojo.query("#" + BGA_Player_Board_Id_Prefix + playerId + " .sw_coins");
            if (refNode && refNode[0]) {
                this.dojo.place(
                    "<div id='" + Player_War_Score_Id_Prefix + playerId + "' class='pythia_player_war_score'>" +
                    "<i class='fa fa-star'></i><span>1 (1)</span></div>",
                    refNode[0],
                    "first");
            }
        }

        // Skip card container for main player and if already rendered
        if (playerId == this.mainPlayer || this.dojo.byId(Player_Cards_Id_Prefix + playerId)) {
            return;
        }
        // Insert card container on player board
        this.dojo.place("<div id='" + Player_Cards_Id_Prefix + playerId + "'" +
            " class='" + Player_Cards_Div_Class + "'></div>",
            BGA_Player_Board_Id_Prefix + playerId,
            "first");
    },

    // Render player hands
    renderPlayerCards: function() {
        const keys = Object.keys(this.players);
        for (const playerId of keys) {
            if (playerId == this.mainPlayer || isObjectEmpty(this.players[playerId].hand)) {
                continue;
            }

            var cardsHTML = "";
            var left = 1;
            for (var card in this.players[playerId].hand) {
                var playedCard = this.game.card_types[this.players[playerId].hand[card].type];
                var posX = -playedCard.backx;
                var posY = -playedCard.backy;
                const cardHtmlId = Player_Hand_Card_Id_Prefix + card;

                cardsHTML += "<div id='" + cardHtmlId + "' style='left: " + left + "px;'>"
                cardsHTML += "<div style='background-position: " + posX + "px " + posY + "px;'>";
                cardsHTML += "<span>" + playedCard.nametr + "</span></div></div>";

                left += 79;
            }
            this.dojo.place(cardsHTML, Player_Cards_Id_Prefix + playerId, "only");
        }
    },


    // Render tooltips for cards in player hands
    renderPlayerCardTooltips: function() {
        const keys = Object.keys(this.players);
        for (const playerId of keys) {
            if (playerId == this.mainPlayer || isObjectEmpty(this.players[playerId].hand)) {
                continue;
            }
            for (var card in this.players[playerId].hand) {
                const tooltipId = "player_hand_item_" + card;

                // Game correctness check
                if (!window.parent.gameui.addTooltipHtml || !window.parent.gameui.tooltips ||
                    !window.parent.gameui.tooltips[tooltipId] || !window.parent.gameui.tooltips[tooltipId].label) {
                    continue;
                }

                window.parent.gameui.addTooltipHtml(Player_Hand_Card_Id_Prefix + card, window.parent.gameui.tooltips[tooltipId].label);
            }
        }
    },

    // Update total player score
    renderPlayerScore: function(playerId, score = 0) {
        if (this.isFinished) {
            return;
        }

        var playerScore = this.dojo.byId(Player_Score_Id_Prefix + playerId);
        if (playerScore) {
            const totalScore = this.players[playerId].bgaScore + this.players[playerId].warScore;
            playerScore.innerHTML = " (" + totalScore + ")";
            this.dojo.query("#" + Player_War_Score_Id_Prefix + playerId + " span")[0]
                .innerHTML = this.players[playerId].bgaScore + " (" + totalScore + ")";
        }
    },

    // Add border and position of leader and runnerup players
    renderLeaderRunnerup: function() {
        if (!this.settings.enableRichBoards || this.isFinished) {
            return;
        }

        // Clean previous leader & runnerup
        this.dojo.query("." + Player_Leader_Class + ", ." + Player_Runnerup_Class)
            .removeClass([Player_Leader_Class, Player_Runnerup_Class]);

        // Find leader and runner ups
        var totalScores = [];
        const keys = Object.keys(this.players);
        for (const playerId of keys) {
            totalScores.push(
                [playerId,
                    this.players[playerId].bgaScore + this.players[playerId].warScore,
                    this.players[playerId].coins
                ]);
        }

        totalScores.sort(function(a, b) {
            return b[1] - a[1] || b[2] - a[2];;
        });

        // Mark new ones
        this.dojo.addClass(BGA_Player_Board_Id_Prefix + totalScores[0][0], Player_Leader_Class);
        this.dojo.addClass(BGA_Player_Board_Id_Prefix + totalScores[1][0], Player_Runnerup_Class);
    },

    // Add laurel wreath and coins icons on top of the card container
    renderCardPoints: function(cardId, isDiscard = false, pointsWorth = null, coinsWorth = null) {
        // Leave if card has no worth info
        if (pointsWorth === null && coinsWorth === null) {
            return;
        }

        // Clean up previous worth in case we saw this card already
        const containerId = isDiscard ? Discard_Card_Worth_Id_Prefix + cardId : Card_Worth_Id_Prefix + cardId;
        this.dojo.destroy(containerId);

        // Build HTML
        const extraClass = this.settings.enableCardPoints ? "" : "pythia_hidden";
        var html = "<span id='" + containerId + "' class='" + Card_Worth_Class + " " + extraClass + "'>";
        if (coinsWorth !== null) {
            html += "<img class='" + Card_Worth_Coins_Class + "' src='" + Coins_Image[coinsWorth] + "' />";
        }
        if (pointsWorth !== null) {
            html += "<img src='" + Victory_Points_Image[pointsWorth] + "' />";
        }
        html += "</span>";

        if (isDiscard) {
            this.dojo.place(html, "discarded_item_" + cardId, "only");
        } else {
            this.dojo.place(html, "cardmenu_" + cardId, "after");
        }
    },

    // Render shields icon next to player coins
    renderMilitaryPower: function(playerId) {
        if (!playerId || !this.players[playerId] || this.isFinished) {
            return;
        }

        var container = this.dojo.query("#" + Player_Military_Power_Id_Prefix + playerId + " span");
        if (container[0]) {
            container[0].innerHTML = this.players[playerId].shields;
        }
    },

    // Render Pythia menu
    renderPythiaMenu: function() {
        var menuHtml = "<div id='pythia_menu'>";
        menuHtml += "<div class='menu_header'>";
        menuHtml += "<h3>PYTHIA v" + GM_info.script.version + "</h3>";

        // Show or hide Pythia menu based on the setting
        if (this.settings.showMenu) {
            menuHtml += "<div id='pythia_menu_list_toggle' class='toggle_container'><a href='javascript:void(0)' class='collapser' data-target='#pythia_menu .menu_content'>(collapse menu)</a>";
            menuHtml += "<a href='javascript:void(0)' class='expander pythia_hidden' data-target='#pythia_menu .menu_content'>(expand menu)</a>";
            menuHtml += "</div></div>";
            menuHtml += "<div class='menu_content'>";
        } else {
            menuHtml += "<div id='pythia_menu_list_toggle' class='toggle_container'><a href='javascript:void(0)' class='collapser pythia_hidden' data-target='#pythia_menu .menu_content'>(collapse menu)</a>";
            menuHtml += "<a href='javascript:void(0)' class='expander' data-target='#pythia_menu .menu_content'>(expand menu)</a>";
            menuHtml += "</div></div>";
            menuHtml += "<div class='menu_content pythia_hidden'>";
        }

        // Card Points setting
        menuHtml += "<div id='pythia_menu_cardpoints' class='menu_item'><span class='title'>Cards Worth:</span>";
        menuHtml += "<span class='status'>Enabled</span><button type='button'>Disable</button></div>";

        // Rich Boards setting
        menuHtml += "<div id='pythia_menu_richboards' class='menu_item'><span class='title'>Rich Boards:</span>";
        menuHtml += "<span class='status'>Enabled</span><button type='button'>Disable</button></div>";

        // War scores setting
        menuHtml += "<div id='pythia_menu_warscores' class='menu_item'><span class='title'>War Scores:</span>";
        menuHtml += "<span class='status'>Enabled</span><button type='button'>Disable</button></div>";

        // HD images setting
        menuHtml += "<div id='pythia_menu_hd' class='menu_item'><span class='title'>HD Images:</span>";
        menuHtml += "<span class='status'>Enabled</span><button type='button'>Disable</button></div>";

        // Fullwidth setting
        menuHtml += "<div id='pythia_menu_fullwidth' class='menu_item'><span class='title'>Full Width:</span>";
        menuHtml += "<span class='status'>Enabled</span><button type='button'>Disable</button></div>";

        menuHtml += "</div>";
        menuHtml += "</div>";
        this.dojo.place(menuHtml, "logs_wrap", "before");

        // Set correct texts based on settings
        this.togglePythiaSettingText("pythia_menu_warscores", this.settings.enableWarScores);
        this.togglePythiaSettingText("pythia_menu_richboards", this.settings.enableRichBoards);
        this.togglePythiaSettingText("pythia_menu_cardpoints", this.settings.enableCardPoints);
        this.togglePythiaSettingText("pythia_menu_hd", this.settings.enableHD);
        this.togglePythiaSettingText("pythia_menu_fullwidth", this.settings.enableFullwidth);

        // Connect event handlers
        this.dojo.connect(this.dojo.query("button", "pythia_menu_warscores")[0], "onclick", this, "togglePythiaSettingWarScores");
        this.dojo.connect(this.dojo.query("button", "pythia_menu_richboards")[0], "onclick", this, "togglePythiaSettingRichBoards");
        this.dojo.connect(this.dojo.query("button", "pythia_menu_cardpoints")[0], "onclick", this, "togglePythiaSettingCardPoints");
        this.dojo.connect(this.dojo.query("button", "pythia_menu_hd")[0], "onclick", this, "togglePythiaSettingHD");
        this.dojo.connect(this.dojo.query("button", "pythia_menu_fullwidth")[0], "onclick", this, "togglePythiaSettingFullwidth");

        // Expand & collapse menu handlers
        this.dojo.connect(this.dojo.query(".menu_header .collapser")[0], "onclick", this, "togglePythiaSettingMenu");
        this.dojo.connect(this.dojo.query(".menu_header .expander")[0], "onclick", this, "togglePythiaSettingMenu");
        this.dojo.connect(this.dojo.query(".menu_header .collapser")[0], "onclick", this, "toggleCollapserExpander");
        this.dojo.connect(this.dojo.query(".menu_header .expander")[0], "onclick", this, "toggleCollapserExpander");
    },

    // Enable or disable display of cards in player hands
    togglePythiaSettingPlayerCardsDisplay: function(pleaseShow) {
        if (pleaseShow) {
            this.dojo.query("." + Player_Cards_Div_Class).removeClass("pythia_hidden");
            this.dojo.query(".sw_coins", "boardspaces").addClass("pythia_player_cards_enabled");
        } else {
            this.dojo.query("." + Player_Cards_Div_Class).addClass("pythia_hidden");
            this.dojo.query(".sw_coins", "boardspaces").removeClass("pythia_player_cards_enabled");
        }
    },

    // Enable or disable display of war scores
    togglePythiaSettingWarScores: function(event) {
        this.settings.enableWarScores = !this.settings.enableWarScores;
        localStorage.setItem("pythia-seetings-warscores", this.settings.enableWarScores);
        this.togglePythiaSettingWarScoresDisplay(this.settings.enableWarScores);
        this.togglePythiaSettingText(event.target.parentNode.id, this.settings.enableWarScores);
    },
    togglePythiaSettingWarScoresDisplay: function(pleaseShow) {
        if (pleaseShow) {
            this.dojo.query("." + Player_Score_Span_Class).removeClass("pythia_hidden");
        } else {
            this.dojo.query("." + Player_Score_Span_Class).addClass("pythia_hidden");
        }
    },

    // Enable or disable display of leader and runnerup positions
    togglePythiaSettingRichBoards: function(event) {
        this.settings.enableRichBoards = !this.settings.enableRichBoards;
        localStorage.setItem("pythia-seetings-richboards", this.settings.enableRichBoards);
        this.togglePythiaSettingRichBoardsDisplay(this.settings.enableRichBoards);
        this.togglePythiaSettingText(event.target.parentNode.id, this.settings.enableRichBoards);
    },
    togglePythiaSettingRichBoardsDisplay: function(pleaseShow) {
        if (pleaseShow) {
            this.renderLeaderRunnerup();
            this.dojo.query(".pythia_player_war_score").removeClass("pythia_hidden");
            this.dojo.query(".pythia_player_military_power").removeClass("pythia_hidden");
        } else {
            this.dojo.query("." + Player_Leader_Class + ", ." + Player_Runnerup_Class)
                .removeClass([Player_Leader_Class, Player_Runnerup_Class]);

            this.dojo.query(".pythia_player_war_score").addClass("pythia_hidden");
            this.dojo.query(".pythia_player_military_power").addClass("pythia_hidden");
        }
    },

    // Enable or disable display of cards points worth
    togglePythiaSettingCardPoints: function(event) {
        this.settings.enableCardPoints = !this.settings.enableCardPoints;
        localStorage.setItem("pythia-seetings-cardpoints", this.settings.enableCardPoints);
        this.togglePythiaSettingCardPointsDisplay(this.settings.enableCardPoints);
        this.togglePythiaSettingText(event.target.parentNode.id, this.settings.enableCardPoints);
    },
    togglePythiaSettingCardPointsDisplay: function(pleaseShow) {
        if (pleaseShow) {
            this.dojo.query("." + Card_Worth_Class).removeClass("pythia_hidden");
        } else {
            this.dojo.query("." + Card_Worth_Class).addClass("pythia_hidden");
        }
    },

    // Enable or disable HD graphics
    togglePythiaSettingHD: function(event) {
        this.settings.enableHD = !this.settings.enableHD;
        localStorage.setItem("pythia-seetings-hd", this.settings.enableHD);
        this.togglePythiaSettingHDDisplay(this.settings.enableHD);
        this.togglePythiaSettingText(event.target.parentNode.id, this.settings.enableHD);
    },
    togglePythiaSettingHDDisplay: function(pleaseShow) {
        this.dojo.query("body").toggleClass("pythia_hd", pleaseShow);
    },

    // Hide or show elements for fullwidth mode
    togglePythiaSettingFullwidth: function(event) {
        this.settings.enableFullwidth = !this.settings.enableFullwidth;
        localStorage.setItem("pythia-seetings-fullwidth", this.settings.enableFullwidth);
        this.togglePythiaSettingFullwidthDisplay(this.settings.enableFullwidth);
        this.togglePythiaSettingText(event.target.parentNode.id, this.settings.enableFullwidth);
    },
    togglePythiaSettingFullwidthDisplay: function(pleaseShow) {
        this.dojo.query("body").toggleClass("pythia_fullwidth", pleaseShow);
        this.dojo.toggleClass("right-side-first-part", "pythia_hidden", pleaseShow);
        this.dojo.toggleClass("logs_wrap", "pythia_hidden", pleaseShow);
    },

    togglePythiaSettingMenu: function(event) {
        this.settings.showMenu = event.target.className == "expander";
        localStorage.setItem("pythia-seetings-showmenu", this.settings.showMenu);
    },

    // Switch enable/disable text in Pythia settings
    togglePythiaSettingText: function(parentId, isEnabled) {
        if (isEnabled) {
            this.dojo.query(".status", parentId)
                .addClass("enabled")
                .removeClass("disabled")[0]
                .innerHTML = "Enabled";
            this.dojo.query("button", parentId)[0].innerHTML = "Disable";
        } else {
            this.dojo.query(".status", parentId)
                .addClass("disabled")
                .removeClass("enabled")[0]
                .innerHTML = "Disabled";
            this.dojo.query("button", parentId)[0].innerHTML = "Enable";
        }
    },

    // Toggle hide / show of any visual element given data-target
    toggleCollapserExpander: function(event) {
        this.dojo.query(".collapser, .expander", event.target.parentNode).toggleClass("pythia_hidden");
        this.dojo.query(event.target.getAttribute('data-target')).toggleClass("pythia_hidden");
    },

    // Is this the first turn of the age?
    isFirstTurn: function() {
        return isObjectEmpty(this.players[this.mainPlayer].hand);
    },

    // Set Pythia CSS styles
    setStyles: function() {
        this.dojo.query("body").addClass("pythia_enabled");
        this.dojo.place(
            "<style type='text/css' id='Pythia_Styles'>" +
            // Generic settings
            ".pythia_player_cards_enabled.sw_coins { top: 50px; } " +
            ".pythia_enabled.arena_mode .player_elo_wrap { visibility: visible; }" +
            ".pythia_enabled #player_board_wrap_" + this.mainPlayer + " .sw_coins { top: 0px; } " +
            ".pythia_enabled #player_hand_wrap { padding-top: 58px; } " +
            ".pythia_enabled #discarded_wrap h3 { padding-bottom: 60px; } " +
            ".pythia_enabled #howto_tutorial { display: none; } " +
            ".pythia_enabled .pythia_hidden { display: none; } " +
            ".pythia_enabled .toggle_container { display: inline; } " +

            // Pythia menu
            "#pythia_menu { font-size: 14px; padding: 10px; position: relative; background-color: #e9d6bf; border: 1px solid black; border-radius: 5px; } " +
            "#pythia_menu .menu_header h3 { display: inline; } " +
            "#pythia_menu .menu_header a { margin-left: 5px; } " +
            "#pythia_menu .menu_content { margin-top: 10px; } " +
            "#pythia_menu .menu_item { height: 26px; } " +
            "#pythia_menu .menu_item span.title { width: 90px; display: inline-block;} " +
            "#pythia_menu .menu_item span.status { text-align: center; width: 55px; display: inline-block; } " +
            "#pythia_menu .menu_item span.status.enabled { color: green; } " +
            "#pythia_menu .menu_item span.status.disabled { color: red; } " +
            "#pythia_menu .menu_item button { width: 60px; padding: 3px; border-radius: 5px; margin-left: 10px; } " +

            // Player cards
            "." + Player_Cards_Div_Class + " { display: block; height: 50px; } " +
            "." + Player_Cards_Div_Class + " div { position: absolute; top: 11px; } " +
            "." + Player_Cards_Div_Class + " div div { background-image: url(" + Cards_Image + "); width: 128px; height: 45px; zoom: 0.6; } " +
            "." + Player_Cards_Div_Class + " div div span { width: 100%; text-align: center; position: absolute; left: 0; top: -25px; font-size: 18px; color: black; cursor: default; } " +

            // Rich boards
            "." + Player_Leader_Class + ", ." + Player_Runnerup_Class + " { border: 5px solid; } " +
            "." + Player_Leader_Class + " { border-color: green; } " +
            "." + Player_Runnerup_Class + " { border-color: red; border-style: inset; } " +
            "." + Player_Leader_Class + " h3::before, ." + Player_Runnerup_Class + " h3::before { float: left; margin-top: -4px; white-space: pre; }" +
            "." + Player_Leader_Class + " h3::before { content: '(Leader) '; color: green; }" +
            "." + Player_Runnerup_Class + " h3::before { content: '(Runner up) '; color: red; }" +
            ".pythia_player_military_power { display: inline; position: relative; top: 3px; }" +
            ".pythia_player_military_power img { width: 30px; padding: 0 4px; }" +
            ".pythia_player_military_power span { position: relative; top: -7px; }" +
            ".pythia_player_war_score { display: inline; padding-right: 4px; position: relative; top: -1px; }" +
            ".pythia_player_war_score i { font-size: 32px; }" +
            ".pythia_player_war_score span { padding-left: 4px; position: relative; top: -3px; }" +

            // Cards worth
            "." + Card_Worth_Class + " { position: absolute; top: -53px; left: 6px; width: 128px; text-align: center; }" +
            "." + Card_Worth_Class + " img { width: 48px; }" +
            "." + Card_Worth_Class + " img." + Card_Worth_Coins_Class + " { position: relative; top: -3px; }" +

            // War scores
            "." + Player_Score_Span_Class + " { display: inline; }" +

            // Fullwidth view
            ".pythia_fullwidth #left-side { margin-right: 0px; } " +

            // New edition styles
            ".new_edition ." + Player_Cards_Div_Class + " div div { background-image: url(" + Cards_Image_V2 + "); width: 255px; height: 110px; zoom: 0.3; text-align: center;} " +
            ".new_edition ." + Player_Cards_Div_Class + " div div span { font-size: 18px; top: 7px; } " +
            ".new_edition .last_board_item { padding-right: 2px; } " +
            ".new_edition .last_board_item .board_item { border-width: 4px; margin: -2px 0 0 -2px; border-color: greenyellow; border-style: outset; } " +
            ".new_edition .last_step_item { border-width: 4px; margin: -4px 0 0 -4px; border-color: greenyellow; border-style: outset; } " +

            // New edition HD boards
            "#pythia_menu_hd { display: none; } " +
            ".new_edition #pythia_menu_hd { display: block; } " +
            ".new_edition.pythia_hd .wonder_face, .new_edition.pythia_hd .player_board_wonder { background-size: 450px 3122px; background-image: url(" + HD_Boards + "); }" +

            "</style>", "sevenwonder_wrap", "last");
    }
};

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function isObjectEmpty(object) {
    return typeof(object) == "undefined" ||
        (Object.keys(object).length === 0 && object.constructor === Object);
}

// Everything starts here

// Everything starts here
var onload = async function() {
    if (Is_Inside_Game) {
        await sleep(3000); // Wait for BGA to load dojo and 7W scripts
        if (!window.parent || !window.parent.gameui || !window.parent.gameui.game_name ||
            window.parent.gameui.game_name != "sevenwonders") {
            return;
        }

        // Prevent multiple launches
        if (window.parent.isPythiaStarted) {
            return;
        } else {
            if (Enable_Logging) console.log("PYTHIA: I have come to serve you");
            window.parent.isPythiaStarted = true;
            window.parent.pythia = pythia.init();
        }
    }
};

if (document.readyState === "complete") {
    onload();
} else {
    (addEventListener || attachEvent).call(window, addEventListener ? "load" : "onload", onload);
}