Poker Odds Calculator

Show poker hand odds on TC

// ==UserScript==
// @name         Poker Odds Calculator
// @namespace    https://openuserjs.org/users/torn/pokerodds
// @version      1.3.37
// @description  Show poker hand odds on TC
// @author       Torn Community
// @match        https://www.torn.com/page.php?sid=holdem
// @run-at       document-body
// @license MIT
// @grant        none
// ==/UserScript==

let GM_addStyle = function(s)
{
    let style = document.createElement("style");
    style.type = "text/css";
    style.innerHTML = s;
    
    document.head.appendChild(style);
}

class Utils
{
    static async sleep(ms)
    {
        return new Promise(e => setTimeout(e, ms));
    }
    
}

class PokerCalculatorModule
{
    constructor()
    {
        this.upgradesToShow = 10;
        this.lastLength = 0;
        this.addStyle();
    }
    
    update()
    {
        //console.time("Update");
        
        let allCards = this.getFullDeck();
        
        let knownCards = Array.from(document.querySelectorAll("[class*='flipper___'] > div[class*='front___'] > div")).map(e => 
        {
          //<div class="fourColors___ihYdi clubs-10___SmzgY cardSize___BbTMe"></div>
          //Desire: "hearts-4", "clubs-14"
            var card = (e.classList[1] || "null-0").split("_")[0]
            .replace("-A", "-14")
            .replace("-K", "-13")
            .replace("-Q", "-12")
            .replace("-J", "-11");
            if(card == "cardSize") {
              card = "null-0";
            }
            return card;
        });
        
        let communityCards = knownCards.slice(0, 5);
        
        allCards = this.filterDeck(allCards, knownCards.filter(e => !e.includes("null")));
        
        if(JSON.stringify(knownCards).length != this.lastLength)
        {
            let playerNodes = document.querySelectorAll("[class*='playerMeGateway___']");
            
            document.querySelector("#pokerCalc-myHand tbody").innerHTML = "";
            document.querySelector("#pokerCalc-upgrades tbody").innerHTML = "";
            document.querySelector("#pokerCalc-oppPossHands tbody").innerHTML = "";

            
            playerNodes.forEach(player =>
            {
                let myCards = Array.from(player.querySelectorAll("div[class*='front___'] > div")).map(e => 
                {
                    var card = (e.classList[1] || "null-0").split("_")[0]
                    .replace("-A", "-14")
                    .replace("-K", "-13")
                    .replace("-Q", "-12")
                    .replace("-J", "-11");

                  if(card == "cardSize") {
                    card = "null-0";
                  }
                    return card;
                });
                
                let myHand = this.getHandScore(communityCards.concat(myCards));

                if(myHand.score > 0)
                {
                    let myUpgrades = {};
                    let bestOppHands = {};
                    let additionalCards = [];
                    let additionalOppCards = [];

                    let myRank = this.calculateHandRank(myHand, communityCards, allCards);

                    if(communityCards.filter(e => !e.includes("null")).length == 3)
                    {
                        for(let a of allCards)
                        {
                            for(let b of allCards)
                            {
                                if(a > b)
                                {
                                    additionalCards.push([a, b]);
                                }
                            }
                        }
                    }
                    else if(communityCards.filter(e => !e.includes("null")).length == 4)
                    {
                        for(let a of allCards)
                        {
                            additionalCards.push([a]);
                        }
                    }
                    else if(communityCards.filter(e => !e.includes("null")).length == 5)
                    {
                        for(let a of allCards)
                        {
                            for(let b of allCards)
                            {
                                if(a > b)
                                {
                                    additionalOppCards.push([a, b]);
                                }
                            }
                        }
                    }

                    /******* My Hand *******/
                    document.querySelector("#pokerCalc-myHand tbody").innerHTML += `<tr><td>Me</td><td>${myHand.description}</td><td>${myRank.rank}</td><td>${myRank.top}</td></tr>`;
                    
                    /******* My Upgrades *******/
                    for(let cards of additionalCards)
                    {
                        let thisHand = this.getHandScore(communityCards.concat(cards).concat(myCards));
                        if(thisHand.score > myHand.score)
                        {
                            let type = thisHand.description.split(":")[0];
                        
                            if(thisHand.description.includes("Four of a kind") || thisHand.description.includes("Three of a kind") || thisHand.description.includes("Pair"))
                            {
                                type += ": " + thisHand.description.split("</span>")[1].split("<span")[0].trim() + "s";
                            }
                            else if(thisHand.description.includes("Full house"))
                            {
                                type += ": " + thisHand.description.split("</span>")[1].split("<span")[0].trim() + "s full of " + thisHand.description.split("</span>").reverse()[0].split("</td>")[0] + "s";
                            }
                            else if(thisHand.description.includes("Straight"))
                            {
                                type += ": " + thisHand.description.split("</span>")[1].split("<span")[0].trim() + "-high";
                            }
                            else if(thisHand.description.includes("Two pairs"))
                            {
                                type += ": " + thisHand.description.split("</span>")[1].split("<span")[0].trim() + "s and " + thisHand.description.split("</span>")[3].split("<span")[0].trim() + "s";
                            }
                            
                            if(!myUpgrades.hasOwnProperty(type))
                            {
                                myUpgrades[type] = {hand: thisHand, type: type, cards: cards, score: thisHand.score, duplicates: 0, chance: 0};
                            }
                            
                            myUpgrades[type].description = thisHand.description;
                            myUpgrades[type].duplicates++;
                        }
                    }

                    let topUpgrades = Object.values(myUpgrades);
        
                    topUpgrades.forEach(e => 
                    {
                        e.chance = ((e.duplicates / additionalCards.length)*100);
                    });
                    
                    // Order by top hands
                    //topUpgrades = Object.values(topUpgrades).sort((a, b) => b.score - a.score).slice(0, this.upgradesToShow);
                    
                    // Order by chance
                    topUpgrades = Object.values(topUpgrades).sort((a, b) => b.chance - a.chance).slice(0, this.upgradesToShow);
                    
                    topUpgrades.forEach(e => 
                    {
                        if(!e.rank)
                        {
                            let thisRank = this.calculateHandRank(e.hand, communityCards.concat(e.cards), this.filterDeck(allCards, e.cards));
                            
                            e.rank = thisRank.rank;
                            e.top = thisRank.top;
                            e.topNumber = thisRank.topNumber;
                        }
                    });
                    
                    let upgradeString = "";
                    
                    for(let upgrade of topUpgrades)
                    {
                        upgradeString += "<tr>";
                        upgradeString += `<td>${upgrade.chance.toFixed(2)}%</td><td>${upgrade.type}</td><td>${upgrade.rank}</td><td>${upgrade.top}</td>`;
                        upgradeString += "</tr>"
                    }
                    
                    document.querySelector("#pokerCalc-upgrades tbody").innerHTML = upgradeString;


                    /******* Best Opp Hands *******/

                    for(let cards of additionalOppCards)
                    {
                        let oppPossHand = this.getHandScore(communityCards.concat(cards));
                        let type = oppPossHand.description.split(":")[0];
                    
                        if(oppPossHand.description.includes("Four of a kind") || oppPossHand.description.includes("Three of a kind") || oppPossHand.description.includes("Pair"))
                        {
                            type += ": " + oppPossHand.description.split("</span>")[1].split("<span")[0].trim() + "s";
                        }
                        else if(oppPossHand.description.includes("Full house"))
                        {
                            type += ": " + oppPossHand.description.split("</span>")[1].split("<span")[0].trim() + "s full of " + oppPossHand.description.split("</span>").reverse()[0].split("</td>")[0] + "s";
                        }
                        else if(oppPossHand.description.includes("Straight"))
                        {
                            type += ": " + oppPossHand.description.split("</span>")[1].split("<span")[0].trim() + "-high";
                        }
                        else if(oppPossHand.description.includes("Two pairs"))
                        {
                            type += ": " + oppPossHand.description.split("</span>")[1].split("<span")[0].trim() + "s and " + oppPossHand.description.split("</span>")[3].split("<span")[0].trim() + "s";
                        }
                        
                        if(!bestOppHands.hasOwnProperty(type))
                        {
                            bestOppHands[type] = {hand: oppPossHand, type: type, cards: cards, score: oppPossHand.score, duplicates: 0, chance: 0};
                        }
                        
                        bestOppHands[type].description = oppPossHand.description;
                        bestOppHands[type].duplicates++;
                    }
                    
                    let topOppHands = Object.values(bestOppHands);
        
                    topOppHands.forEach(e => 
                    {
                        e.chance = ((e.duplicates / additionalOppCards.length)*100);
                    });
                    
                    // Order by top hands
                    topOppHands = Object.values(topOppHands).sort((a, b) => b.score - a.score).slice(0, this.upgradesToShow);
                    
                    // Order by chance
                    //topOppHands = Object.values(topOppHands).sort((a, b) => b.chance - a.chance).slice(0, this.upgradesToShow);
                    
                    topOppHands.forEach(e => 
                    {
                        let thisRank = this.calculateHandRank(e.hand, communityCards.concat(e.cards), this.filterDeck(allCards, e.cards));
                        
                        e.rank = thisRank.rank;
                        e.top = thisRank.top;
                        e.topNumber = thisRank.topNumber;
                    });
                        
                    let oppHandString = "";
                    
                    for(let upgrade of topOppHands)
                    {
                        oppHandString += "<tr>";
                        oppHandString += `<td>${upgrade.chance.toFixed(2)}%</td><td>${upgrade.type}</td><td>${upgrade.rank}</td><td>${upgrade.top}</td>`;
                        oppHandString += "</tr>"
                    }
                    
                    document.querySelector("#pokerCalc-oppPossHands tbody").innerHTML = oppHandString;

                }
            });

            let playerRows = Array.from(document.querySelectorAll("#pokerCalc-div #pokerCalc-myHand tr")).slice(1);
            
            if(playerRows.length > 0)
            {
                playerRows.reduce((a, b) => parseFloat(a.children[3].innerText.replace(/[^0-9\.]/g, "")) <= parseFloat(b.children[3].innerText.replace(/[^0-9\.]/g, "")) ? a : b).style.background = "#dfd";
            }
            
            let upgradeRows = Array.from(document.querySelectorAll("#pokerCalc-div #pokerCalc-upgrades tr")).slice(1);
            
            if(upgradeRows.length > 0)
            {
                upgradeRows.reduce((a, b) => parseFloat(a.children[3].innerText.replace(/[^0-9\.]/g, "")) <= parseFloat(b.children[3].innerText.replace(/[^0-9\.]/g, "")) ? a : b).style.background = "#dfd";
            }

            let bestOppHandRows = Array.from(document.querySelectorAll("#pokerCalc-div #pokerCalc-oppPossHands tr")).slice(1);
            
            if(bestOppHandRows.length > 0)
            {
                bestOppHandRows.reduce((a, b) => parseFloat(a.children[3].innerText.replace(/[^0-9\.]/g, "")) <= parseFloat(b.children[3].innerText.replace(/[^0-9\.]/g, "")) ? a : b).style.background = "#dfd";
            }


            
            
            this.lastLength = JSON.stringify(knownCards).length;
        }
        
        //console.timeEnd("Update");
        
        setTimeout(this.update.bind(this), 500);
    }
    
    addStyle()
    {
        GM_addStyle(`
            #pokerCalc-div *
            {
                all: revert;
            }
            
            #pokerCalc-div
            {
                background-color: #eee;
                color: #444;
                padding: 5px;
                margin-top: 10px;
            }
            
            #pokerCalc-div table
            {
                border-collapse: collapse;
                margin-top: 10px;
                width: 100%;
            }
            
            #pokerCalc-div th, #pokerCalc-div td
            {
                border: 1px solid #444;
                padding: 5px;
                width: 25%;
            }
            
            #pokerCalc-div tr td:nth-child(1), #pokerCalc-div tr td:nth-child(3), #pokerCalc-div tr td:nth-child(4)
            {
                text-align: center;
            }
            
            #pokerCalc-div caption
            {
                margin-bottom: 2px;
                font-weight: 600;
            }
        `);
    }
    
    addStatisticsTable()
    {

        while(!(root = document.querySelector("#react-root"))) {
            setTimeout(function(){
                window.pokerCalculator.addStatisticsTable();
            }, 1000);
            return;
        }

        if (!document.getElementById("pokerCalc-div")) {
            var pokercalcdiv = document.createElement("div");
            pokercalcdiv.id = "pokerCalc-div";
            var root = document.querySelector("#react-root");
            root.after(pokercalcdiv);
        }

        let div = document.getElementById("pokerCalc-div");

        div.innerHTML = `
        <table id="pokerCalc-myHand">
        <caption>Your Hand</caption>
        <thead>
        <tr>
            <th>Name</th>
            <th>Hand</th>
            <th>Rank</th>
            <th>Top</th>
        </tr>
        </thead>
        <tbody>
        
        </tbody>
        </table>
        
        <table id="pokerCalc-upgrades">
        <caption>Your Potential Hands</caption>
        <thead>
        <tr>
            <th>Chance</th>
            <th>Hand</th>
            <th>Rank</th>
            <th>Top</th>
        </tr>
        </thead>
        <tbody>
        
        </tbody>
        </table>

        <table id="pokerCalc-oppPossHands">
        <caption>Opponent Potential Hands</caption>
        <thead>
        <tr>
            <th>Chance</th>
            <th>Hand</th>
            <th>Rank</th>
            <th>Top</th>
        </tr>
        </thead>
        <tbody>
        
        </tbody>
        </table>
        `;
        
        this.update();
    }
    
    prettifyHand(hand)
    {
        let resultText = "";
        
        for(let card of hand)
        {
            if(card != "null-0")
            {
                resultText += " " + card
                                    .replace("diamonds", "<span style='color: red'>♦</span>")
                                    .replace("spades", "<span style='color: black'>♠</span>")
                                    .replace("hearts", "<span style='color: red'>♥</span>")
                                    .replace("clubs", "<span style='color: black'>♣</span>")
                                    .replace("-14", "-A")
                                    .replace("-13", "K")
                                    .replace("-12", "Q")
                                    .replace("-11", "J")
                                    .replace("-", "");
            }
        }
        
        return resultText;
    }
    
    getFullDeck()
    {
        let result = [];
        
        for(let suit of ["hearts", "diamonds", "spades", "clubs"])
        {
            for(let value of [2,3,4,5,6,7,8,9,10,11,12,13,14])
            {
                result.push(suit + "-" + value);
            }
        }
        
        return result;
    }
    
    filterDeck(deck, cards)
    {
        for(let card of cards)
        {
            let index = deck.indexOf(card);
            
            if(index != -1)
            {
                delete deck[index];
            }
        }
        
        return deck.filter(e => e != "empty");
    }
    
    calculateHandRank(myHand, communityCards, allCards)
    {
        let otherBetterHands = 0;
        let totalHands = 1;
        
        for(let a of allCards)
        {
            for(let b of allCards)
            {
                if(a > b)
                {
                    let thisHand = this.getHandScore(communityCards.concat([a, b]));
            
                    otherBetterHands += thisHand.score >= myHand.score;
                    totalHands++;
                }
            }
        }
        
        return {rank: `${otherBetterHands+1} / ${totalHands}`, top: `${(((otherBetterHands+1) / totalHands)*100).toFixed(1)}%`, topNumber: (otherBetterHands+1) / totalHands}
    }
    
    getHandScore(hand)
    {
        hand = hand.filter(e => !e.includes("null"));
        
        if(hand.length < 5){return {description: "", score: 0};}
        
        let resultString = "";
        let resultText = "";
        let handResult;
        let handObject = this.makeHandObject(hand);

        if(handResult = this.hasFourOfAKind(hand, handObject))
        {
            resultString += "7";
            resultText += "Four of a kind:";
        }
        else if(handResult = this.hasFullHouse(hand, handObject))
        {
            resultString += "6";
            resultText += "Full house:";
        }
        else if(handResult = this.hasFlush(hand, handObject))
        {
            let isRoyal = this.hasRoyalFlush(hand, handObject);
            
            if(isRoyal)
            {
                handResult = isRoyal;
                resultString += "9";
                resultText += "Royal flush:";
            }
            else
            {
                let isStraight = this.hasStraightFlush(hand, handObject);
                
                if(isStraight)
                {
                    handResult = isStraight;
                    resultString += "8";
                    resultText += "Straight flush:";
                }
                else
                {
                    resultString += "5";
                    resultText += "Flush:";
                }
            }
        }
        else if(handResult = this.hasStraight(hand, handObject))
        {
            resultString += "4";
            resultText += "Straight:";
        }
        else if(handResult = this.hasThreeOfAKind(hand, handObject))
        {
            resultString += "3";
            resultText += "Three of a kind:";
        }
        else if(handResult = this.hasTwoPairs(hand, handObject))
        {
            resultString += "2";
            resultText += "Two pairs:";
        }
        else if(handResult = this.hasPair(hand, handObject))
        {
            resultString += "1";
            resultText += "Pair:";
        }
        else
        {
            resultString += "0";
            resultText += "High card:";
            
            handResult = hand.slice(0, 5);
        }

        for(let card of handResult)
        {
            resultString += parseInt(card.split("-")[1]).toString(16);
        }
        
        resultText += this.prettifyHand(handResult);

        return {description: resultText, result: handResult, score: parseInt(resultString, 16)};
    }
    
    makeHandObject(hand)
    {
        let resultMap = {cards: hand, suits: {}, values: {}};
        
        hand.sort((a, b) => parseInt(b.split("-")[1]) - parseInt(a.split("-")[1])).filter(e => e != "null-0").forEach(e => 
        {
            let suit = e.split("-")[0];
            let value = e.split("-")[1];
            
            if(!resultMap.suits.hasOwnProperty(suit))
            {
                resultMap.suits[suit] = [];
            }
            
            if(!resultMap.values.hasOwnProperty(value))
            {
                resultMap.values[value] = [];
            }
            
            resultMap.suits[suit].push(e);
            resultMap.values[value].push(e);
        });

        return resultMap;
    }
    
    hasRoyalFlush(hand, handObject)
    {
        hand = hand.sort((a, b) => parseInt(b.split("-")[1]) - parseInt(a.split("-")[1]));
        
        let flush = this.hasFlush(hand, handObject);
        let straight = this.hasStraight(hand, handObject);
        
        if(flush && straight)
        {
            let straightSameColor = straight.filter(e => e.split("-")[0] == flush[0].split("-")[0]).length == 5;
            
            if(straightSameColor && hand[0].split("-")[1] == "14")
            {
                return flush;
            }
        }
    }
    
    hasStraightFlush(hand, handObject)
    {
        hand = hand.sort((a, b) => parseInt(b.split("-")[1]) - parseInt(a.split("-")[1]));
        
        let flush = this.hasFlush(hand, handObject);
        let straight = this.hasStraight(hand, handObject);
        
        if(flush && straight)
        {
            let straightSameColor = straight.filter(e => e.split("-")[0] == flush[0].split("-")[0]).length == 5;
            
            if(straightSameColor && hand[0].split("-")[1] != "14")
            {
                return flush;
            }
        }
    }
    
    hasFourOfAKind(hand, handObject)
    {
        let quadruplets = Object.values(handObject.values).filter(e => e.length == 4);

        if(quadruplets.length > 0)
        {
            delete hand[hand.indexOf(quadruplets[0][0])];
            delete hand[hand.indexOf(quadruplets[0][1])];
            delete hand[hand.indexOf(quadruplets[0][2])];
            delete hand[hand.indexOf(quadruplets[0][3])];
            
            hand = hand.filter(e => e != "empty");
            
            return quadruplets[0].concat(hand).slice(0, 5);
        }
    }
    
    hasFullHouse(hand, handObject)
    {
        let pairs = Object.values(handObject.values).filter(e => e.length == 2);
        let triplets = Object.values(handObject.values).filter(e => e.length == 3);
        
        if(pairs.length > 0 && triplets.length > 0)
        {
            delete hand[hand.indexOf(pairs[0][0])];
            delete hand[hand.indexOf(pairs[0][1])];
            delete hand[hand.indexOf(triplets[0][0])];
            delete hand[hand.indexOf(triplets[0][1])];
            
            hand = hand.filter(e => e != "empty");
            
            if(parseInt(pairs[0][0].split("-")[1]) > parseInt(triplets[0][0].split("-")[1]))
            {
                return pairs[0].concat(triplets[0].concat(hand)).slice(0, 5);
            }
            else
            {
                return triplets[0].concat(pairs[0].concat(hand)).slice(0, 5);
            }
        }
    }
    
    hasFlush(hand, handObject)
    {
        let quintuplets = Object.values(handObject.suits).filter(e => e.length == 5);

        if(quintuplets.length == 1)
        {
            return quintuplets[0];
        }
    }

    hasStraight(hand, handObject)
    {
        hand = hand.sort((a, b) => parseInt(b.split("-")[1]) - parseInt(a.split("-")[1]));
        
        let streak = 1;
        let streakCards = [hand[0]];
        
        for(let i = 1; i < hand.length; i++)
        {
            let current = parseInt(hand[i].split("-")[1]);
            let previous = parseInt(hand[i-1].split("-")[1]);

            if(current == previous){continue;}
            
            if(current != (previous - 1) && !(current == 5 && previous == 14))
            {
                streak = 1;
                streakCards = [hand[i]];
            }
            else
            {
                streak++;
                streakCards.push(hand[i]);
            }
            
            if(streak == 5)
            {
                break;
            }
        }
        
        if(streak == 5)
        {
            if(streakCards.some(e => e.includes("-14")) && streakCards.some(e => e.includes("-5")))
            {
                streakCards = streakCards.slice(1).concat(streakCards.slice(0, 1));
            }
            
            return streakCards;
        }
    }
    
    hasThreeOfAKind(hand, handObject)
    {
        let triplets = Object.values(handObject.values).filter(e => e.length == 3);

        if(triplets.length > 0)
        {
            delete hand[hand.indexOf(triplets[0][0])];
            delete hand[hand.indexOf(triplets[0][1])];
            delete hand[hand.indexOf(triplets[0][2])];
            
            hand = hand.filter(e => e != "empty");
            
            return triplets[0].concat(hand).slice(0, 5);
        }
    }
    
    hasTwoPairs(hand, handObject)
    {
        let pairs = Object.values(handObject.values).filter(e => e.length == 2);

        if(pairs.length > 1)
        {
            delete hand[hand.indexOf(pairs[0][0])];
            delete hand[hand.indexOf(pairs[0][1])];
            delete hand[hand.indexOf(pairs[1][0])];
            delete hand[hand.indexOf(pairs[1][1])];
            
            hand = hand.filter(e => e != "empty");
            
            if(parseInt(pairs[0][0].split("-")[1]) > parseInt(pairs[1][0].split("-")[1]))
            {
                return pairs[0].concat(pairs[1].concat(hand)).slice(0, 5);
            }
            else
            {
                return pairs[1].concat(pairs[0].concat(hand)).slice(0, 5);
            }
        }
    }
    
    hasPair(hand, handObject)
    {
        let pairs = Object.values(handObject.values).filter(e => e.length == 2);

        if(pairs.length > 0)
        {
            delete hand[hand.indexOf(pairs[0][0])];
            delete hand[hand.indexOf(pairs[0][1])];
            
            hand = hand.filter(e => e != "empty");
            
            return pairs[0].concat(hand).slice(0, 5);
        }
    }
}

window.pokerCalculator = new PokerCalculatorModule();
window.pokerCalculator.addStatisticsTable()