Poker Odds Calculator

Show poker hand odds on TC

目前为 2025-04-05 提交的版本。查看 最新版本

// ==UserScript==
// @name         Poker Odds Calculator
// @namespace    https://openuserjs.org/users/torn/pokerodds
// @version      1.3.38
// @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==

(function() {
    'use strict';

    // Utility function to add styles
    const addStyle = (css) => {
        const style = document.createElement('style');
        style.type = 'text/css';
        style.textContent = css;
        document.head.appendChild(style);
    };

    // Add required styles
    addStyle(`
        #pokerCalc-div * {
            all: revert;
        }
        
        #pokerCalc-div {
            background-color: #eee;
            color: #444;
            padding: 5px;
            margin-top: 10px;
            font-family: Arial, sans-serif;
        }
        
        #pokerCalc-div table {
            border-collapse: collapse;
            margin-top: 10px;
            width: 100%;
            background: white;
        }
        
        #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;
            color: #333;
        }

        #pokerCalc-div tr:hover {
            background-color: #f5f5f5;
        }

        #pokerCalc-div .best-hand {
            background-color: #dfd;
        }
    `);

    class PokerCalculator {
        constructor() {
            this.upgradesToShow = 10;
            this.lastLength = 0;
            this.setupObserver();
        }

        getFullDeck() {
            const suits = ['hearts', 'diamonds', 'spades', 'clubs'];
            const values = Array.from({length: 13}, (_, i) => i + 2);
            return suits.flatMap(suit => 
                values.map(value => `${suit}-${value}`)
            );
        }

        filterDeck(deck, cards) {
            return deck.filter(card => !cards.includes(card));
        }

        prettifyCard(card) {
            if (card === 'null-0') return '';
            
            const [suit, value] = card.split('-');
            const suitSymbols = {
                'diamonds': '♦',
                'spades': '♠',
                'hearts': '♥',
                'clubs': '♣'
            };
            
            const valueMap = {
                '14': 'A',
                '13': 'K',
                '12': 'Q',
                '11': 'J'
            };

            const displayValue = valueMap[value] || value;
            const color = suit === 'hearts' || suit === 'diamonds' ? 'red' : 'black';
            return `<span style="color: ${color}">${displayValue}${suitSymbols[suit]}</span>`;
        }

        makeHandObject(hand) {
            const resultMap = {
                cards: hand,
                suits: {},
                values: {}
            };

            hand.sort((a, b) => {
                const valueA = parseInt(a.split('-')[1]);
                const valueB = parseInt(b.split('-')[1]);
                return valueB - valueA;
            })
            .filter(card => !card.includes('null'))
            .forEach(card => {
                const [suit, value] = card.split('-');
                
                if (!resultMap.suits[suit]) resultMap.suits[suit] = [];
                if (!resultMap.values[value]) resultMap.values[value] = [];
                
                resultMap.suits[suit].push(card);
                resultMap.values[value].push(card);
            });

            return resultMap;
        }

        // Hand evaluation methods
        hasRoyalFlush(hand, handObject) {
            for (const suit in handObject.suits) {
                const suitCards = handObject.suits[suit];
                if (suitCards.length >= 5) {
                    const values = new Set(suitCards.map(card => 
                        parseInt(card.split('-')[1])
                    ));
                    
                    if ([10,11,12,13,14].every(value => values.has(value))) {
                        return suitCards
                            .filter(card => parseInt(card.split('-')[1]) >= 10)
                            .sort((a, b) => 
                                parseInt(b.split('-')[1]) - parseInt(a.split('-')[1])
                            )
                            .slice(0, 5);
                    }
                }
            }
            return null;
        }

        hasStraightFlush(hand, handObject) {
            for (const suit in handObject.suits) {
                const suitCards = handObject.suits[suit];
                if (suitCards.length >= 5) {
                    const straight = this.hasStraight(suitCards, 
                        this.makeHandObject(suitCards)
                    );
                    if (straight) return straight;
                }
            }
            return null;
        }

        hasFourOfAKind(hand, handObject) {
            const quads = Object.values(handObject.values)
                .find(cards => cards.length === 4);
                
            if (quads) {
                const kickers = hand.filter(card => 
                    !quads.includes(card)
                ).sort((a, b) => 
                    parseInt(b.split('-')[1]) - parseInt(a.split('-')[1])
                );
                
                return [...quads, kickers[0]];
            }
            return null;
        }

        hasFullHouse(hand, handObject) {
            const trips = Object.values(handObject.values)
                .filter(cards => cards.length === 3)
                .sort((a, b) => 
                    parseInt(b[0].split('-')[1]) - parseInt(a[0].split('-')[1])
                );

            if (trips.length === 0) return null;

            for (const three of trips) {
                const threeValue = parseInt(three[0].split('-')[1]);
                const pair = Object.values(handObject.values)
                    .find(cards => 
                        cards.length >= 2 && 
                        parseInt(cards[0].split('-')[1]) !== threeValue
                    );
                    
                if (pair) {
                    return [...three.slice(0, 3), ...pair.slice(0, 2)];
                }
            }
            return null;
        }

        hasFlush(hand, handObject) {
            for (const suit in handObject.suits) {
                if (handObject.suits[suit].length >= 5) {
                    return handObject.suits[suit]
                        .sort((a, b) => 
                            parseInt(b.split('-')[1]) - parseInt(a.split('-')[1])
                        )
                        .slice(0, 5);
                }
            }
            return null;
        }

        hasStraight(hand, handObject) {
            const values = new Map();
            hand.forEach(card => {
                const value = parseInt(card.split('-')[1]);
                if (!values.has(value) || 
                    parseInt(values.get(value).split('-')[1]) < value) {
                    values.set(value, card);
                }
            });

            const uniqueValues = Array.from(values.keys()).sort((a, b) => b - a);

            // Check regular straights
            for (let i = 0; i <= uniqueValues.length - 5; i++) {
                const straight = uniqueValues.slice(i, i + 5);
                if (straight[0] - straight[4] === 4) {
                    return straight.map(value => values.get(value));
                }
            }

            // Check Ace-low straight (A,2,3,4,5)
            if (uniqueValues.includes(14) && 
                uniqueValues.includes(2) && 
                uniqueValues.includes(3) && 
                uniqueValues.includes(4) && 
                uniqueValues.includes(5)) {
                return [
                    values.get(5),
                    values.get(4),
                    values.get(3),
                    values.get(2),
                    values.get(14)
                ];
            }

            return null;
        }

        hasThreeOfAKind(hand, handObject) {
            const trips = Object.values(handObject.values)
                .find(cards => cards.length === 3);
                
            if (trips) {
                const kickers = hand.filter(card => 
                    !trips.includes(card)
                ).sort((a, b) => 
                    parseInt(b.split('-')[1]) - parseInt(a.split('-')[1])
                );
                
                return [...trips, ...kickers.slice(0, 2)];
            }
            return null;
        }

        hasTwoPairs(hand, handObject) {
            const pairs = Object.values(handObject.values)
                .filter(cards => cards.length === 2)
                .sort((a, b) => 
                    parseInt(b[0].split('-')[1]) - parseInt(a[0].split('-')[1])
                );

            if (pairs.length >= 2) {
                const kickers = hand.filter(card => 
                    !pairs[0].includes(card) && !pairs[1].includes(card)
                ).sort((a, b) => 
                    parseInt(b.split('-')[1]) - parseInt(a.split('-')[1])
                );
                
                return [...pairs[0], ...pairs[1], kickers[0]];
            }
            return null;
        }

        hasPair(hand, handObject) {
            const pair = Object.values(handObject.values)
                .find(cards => cards.length === 2);
                
            if (pair) {
                const kickers = hand.filter(card => 
                    !pair.includes(card)
                ).sort((a, b) => 
                    parseInt(b.split('-')[1]) - parseInt(a.split('-')[1])
                );
                
                return [...pair, ...kickers.slice(0, 3)];
            }
            return null;
        }

        getHandScore(hand) {
            const filteredHand = hand.filter(card => !card.includes('null'));
            if (filteredHand.length < 5) return { description: '', score: 0 };

            const handObject = this.makeHandObject(filteredHand);
            let handResult;
            let resultString = '';
            let resultText = '';

            const evaluators = [
                { fn: this.hasRoyalFlush, score: '9', text: 'Royal flush' },
                { fn: this.hasStraightFlush, score: '8', text: 'Straight flush' },
                { fn: this.hasFourOfAKind, score: '7', text: 'Four of a kind' },
                { fn: this.hasFullHouse, score: '6', text: 'Full house' },
                { fn: this.hasFlush, score: '5', text: 'Flush' },
                { fn: this.hasStraight, score: '4', text: 'Straight' },
                { fn: this.hasThreeOfAKind, score: '3', text: 'Three of a kind' },
                { fn: this.hasTwoPairs, score: '2', text: 'Two pairs' },
                { fn: this.hasPair, score: '1', text: 'Pair' }
            ];

            for (const { fn, score, text } of evaluators) {
                handResult = fn.call(this, filteredHand, handObject);
                if (handResult) {
                    resultString = score;
                    resultText = text;
                    break;
                }
            }

            if (!handResult) {
                resultString = '0';
                resultText = 'High card';
                handResult = filteredHand.slice(0, 5);
            }

            handResult.forEach(card => {
                resultString += parseInt(card.split('-')[1]).toString(16);
            });

            return {
                description: `${resultText}: ${handResult.map(card => 
                    this.prettifyCard(card)
                ).join(' ')}`,
                result: handResult,
                score: parseInt(resultString, 16)
            };
        }

        calculateHandRank(myHand, communityCards, allCards) {
            if (!myHand?.score || !Array.isArray(communityCards) || !Array.isArray(allCards)) {
                return {
                    rank: 'N/A',
                    top: 'N/A',
                    topNumber: 0,
                    betterHands: 0,
                    equalHands: 0,
                    worseHands: 0,
                    totalHands: 0
                };
            }

            const availableCards = allCards.filter(card => 
                card && 
                !communityCards.includes(card) && 
                !myHand.result.includes(card)
            );

            let betterHands = 0;
            let equalHands = 0;
            let worseHands = 0;
            let totalHands = 0;

            for (let i = 0; i < availableCards.length - 1; i++) {
                for (let j = i + 1; j < availableCards.length; j++) {
                    const oppHand = this.getHandScore(
                        communityCards.concat([availableCards[i], availableCards[j]])
                    );
                    
                    if (oppHand.score > myHand.score) betterHands++;
                    else if (oppHand.score === myHand.score) equalHands++;
                    else worseHands++;
                    
                    totalHands++;
                }
            }

            if (totalHands === 0) {
                return {
                    rank: 'N/A',
                    top: 'N/A',
                    topNumber: 0,
                    betterHands: 0,
                    equalHands: 0,
                    worseHands: 0,
                    totalHands: 0
                };
            }

            const trueRank = betterHands + Math.ceil(equalHands / 2);
            const percentile = ((betterHands + equalHands/2) / totalHands) * 100;

            return {
                rank: `${trueRank + 1} / ${totalHands}`,
                top: `${percentile.toFixed(1)}%`,
                topNumber: percentile / 100,
                betterHands,
                equalHands,
                worseHands,
                totalHands
            };
        }

        setupObserver() {
            const observer = new MutationObserver(() => {
                if (!document.getElementById('pokerCalc-div')) {
                    this.addStatisticsTable();
                }
                this.update();
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }

        addStatisticsTable() {
            const root = document.querySelector('#react-root');
            if (!root) return;

            const div = document.createElement('div');
            div.id = '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>
            `;
            
            root.after(div);
        }

        update() {
            const allCards = this.getFullDeck();
            
            const knownCards = Array.from(
                document.querySelectorAll("[class*='flipper___'] > div[class*='front___'] > div")
            ).map(e => {
                const card = (e.classList[1] || "null-0").split("_")[0]
                    .replace("-A", "-14")
                    .replace("-K", "-13")
                    .replace("-Q", "-12")
                    .replace("-J", "-11");
                    
                return card === "cardSize" ? "null-0" : card;
            });
            
            const communityCards = knownCards.slice(0, 5);
            const filteredDeck = this.filterDeck(allCards, 
                knownCards.filter(e => !e.includes("null"))
            );

            if (JSON.stringify(knownCards).length === this.lastLength) return;
            this.lastLength = JSON.stringify(knownCards).length;

            const tables = {
                myHand: document.querySelector("#pokerCalc-myHand tbody"),
                upgrades: document.querySelector("#pokerCalc-upgrades tbody"),
                oppHands: document.querySelector("#pokerCalc-oppPossHands tbody")
            };

            if (!tables.myHand || !tables.upgrades || !tables.oppHands) return;

            tables.myHand.innerHTML = '';
            tables.upgrades.innerHTML = '';
            tables.oppHands.innerHTML = '';

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

                const myHand = this.getHandScore(communityCards.concat(myCards));
                if (myHand.score === 0) return;

                const myRank = this.calculateHandRank(myHand, communityCards, filteredDeck);
                
                // Update tables
                this.updateMyHandTable(tables.myHand, myHand, myRank);
                this.updateUpgradesTable(tables.upgrades, myHand, communityCards, myCards, filteredDeck);
                this.updateOpponentHandsTable(tables.oppHands, communityCards, filteredDeck);
            });

            // Highlight best hands in each table
            this.highlightBestHands();
        }

        updateMyHandTable(table, myHand, myRank) {
            table.innerHTML += `
                <tr>
                    <td>Me</td>
                    <td>${myHand.description}</td>
                    <td>${myRank.rank}</td>
                    <td>${myRank.top}</td>
                </tr>
            `;
        }

        updateUpgradesTable(table, myHand, communityCards, myCards, allCards) {
            const upgrades = {};
            const additionalCards = [];

            const communityLength = communityCards.filter(e => !e.includes("null")).length;

            if (communityLength === 3) {
                for (let a of allCards) {
                    for (let b of allCards) {
                        if (a > b) additionalCards.push([a, b]);
                    }
                }
            } else if (communityLength === 4) {
                for (let a of allCards) {
                    additionalCards.push([a]);
                }
            }

            for (let cards of additionalCards) {
                const newHand = this.getHandScore(
                    communityCards.concat(cards).concat(myCards)
                );
                
                if (newHand.score > myHand.score) {
                    const type = this.getHandType(newHand);
                    
                    if (!upgrades[type]) {
                        upgrades[type] = {
                            hand: newHand,
                            type,
                            cards,
                            score: newHand.score,
                            duplicates: 0,
                            chance: 0
                        };
                    }
                    
                    upgrades[type].duplicates++;
                }
            }

            const topUpgrades = Object.values(upgrades);
            topUpgrades.forEach(upgrade => {
                upgrade.chance = (upgrade.duplicates / additionalCards.length) * 100;
                const rank = this.calculateHandRank(
                    upgrade.hand,
                    communityCards.concat(upgrade.cards),
                    this.filterDeck(allCards, upgrade.cards)
                );
                upgrade.rank = rank.rank;
                upgrade.top = rank.top;
            });

            const sortedUpgrades = topUpgrades
                .sort((a, b) => b.chance - a.chance)
                .slice(0, this.upgradesToShow);

            table.innerHTML = sortedUpgrades.map(upgrade => `
                <tr>
                    <td>${upgrade.chance.toFixed(2)}%</td>
                    <td>${upgrade.type}</td>
                    <td>${upgrade.rank}</td>
                    <td>${upgrade.top}</td>
                </tr>
            `).join('');
        }

        updateOpponentHandsTable(table, communityCards, allCards) {
            if (communityCards.filter(e => !e.includes("null")).length !== 5) return;

            const oppHands = {};
            const additionalCards = [];

            for (let a of allCards) {
                for (let b of allCards) {
                    if (a > b) additionalCards.push([a, b]);
                }
            }

            for (let cards of additionalCards) {
                const hand = this.getHandScore(communityCards.concat(cards));
                const type = this.getHandType(hand);
                
                if (!oppHands[type]) {
                    oppHands[type] = {
                        hand,
                        type,
                        cards,
                        score: hand.score,
                        duplicates: 0,
                        chance: 0
                    };
                }
                
                oppHands[type].duplicates++;
            }

            const topHands = Object.values(oppHands);
            topHands.forEach(hand => {
                hand.chance = (hand.duplicates / additionalCards.length) * 100;
                const rank = this.calculateHandRank(
                    hand.hand,
                    communityCards.concat(hand.cards),
                    this.filterDeck(allCards, hand.cards)
                );
                hand.rank = rank.rank;
                hand.top = rank.top;
            });

            const sortedHands = topHands
                .sort((a, b) => b.score - a.score)
                .slice(0, this.upgradesToShow);

            table.innerHTML = sortedHands.map(hand => `
                <tr>
                    <td>${hand.chance.toFixed(2)}%</td>
                    <td>${hand.type}</td>
                    <td>${hand.rank}</td>
                    <td>${hand.top}</td>
                </tr>
            `).join('');
        }

        getHandType(hand) {
            const base = hand.description.split(':')[0];
            const details = hand.description.split('</span>');
            
            if (base.includes('Four of a kind') || 
                base.includes('Three of a kind') || 
                base.includes('Pair')) {
                return `${base}: ${details[1].split('<span')[0].trim()}s`;
            }
            
            if (base.includes('Full house')) {
                return `${base}: ${details[1].split('<span')[0].trim()}s full of ${details.reverse()[0].split('</td>')[0]}s`;
            }
            
            if (base.includes('Straight')) {
                return `${base}: ${details[1].split('<span')[0].trim()}-high`;
            }
            
            if (base.includes('Two pairs')) {
                return `${base}: ${details[1].split('<span')[0].trim()}s and ${details[3].split('<span')[0].trim()}s`;
            }
            
            return base;
        }

        highlightBestHands() {
            ['#pokerCalc-myHand', '#pokerCalc-upgrades', '#pokerCalc-oppPossHands'].forEach(tableId => {
                const rows = Array.from(document.querySelectorAll(`${tableId} tbody tr`));
                if (rows.length === 0) return;

                rows.forEach(row => row.classList.remove('best-hand'));
                
                const bestRow = rows.reduce((a, b) => {
                    const valueA = parseFloat(a.children[3].innerText.replace(/[^0-9\.]/g, ""));
                    const valueB = parseFloat(b.children[3].innerText.replace(/[^0-9\.]/g, ""));
                    return valueA <= valueB ? a : b;
                });
                
                bestRow.classList.add('best-hand');
            });
        }
    }

    // Initialize the calculator
    window.pokerCalculator = new PokerCalculator();
})();