// ==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();
})();