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