poker-odds-calculator
Version:
A pre-flop and post-flop odds calculator for Texas Holdem
224 lines • 8.26 kB
JavaScript
/**
* OddsCalculator
*
*/
import uniqBy from 'lodash/uniqBy.js';
import random from 'lodash/random.js';
import { Card, Suit } from './Card.js';
import { CardGroup } from './CardGroup.js';
import { FullDeckGame, ShortDeckGame } from './Game.js';
import { HandRank } from './HandRank.js';
export class HandEquity {
constructor() {
this.possibleHandsCount = 0;
this.bestHandCount = 0;
this.tieHandCount = 0;
}
addPossibility(isBestHand, isTie) {
this.possibleHandsCount += 1;
if (isBestHand) {
this.bestHandCount += 1;
}
else if (isTie) {
this.tieHandCount += 1;
}
}
getEquity() {
if (this.possibleHandsCount === 0) {
return 0;
}
return Math.round(this.bestHandCount * 100.0 / this.possibleHandsCount);
}
getTiePercentage() {
if (this.possibleHandsCount === 0) {
return 0;
}
return Math.round(this.tieHandCount * 100.0 / this.possibleHandsCount);
}
toString() {
let s = `${this.getEquity()}%`;
const tie = this.getTiePercentage();
if (tie > 0) {
s += ` (Tie: ${tie}%)`;
}
return s;
}
}
export class OddsCalculator {
constructor(equities, handranks, iterations, elapsedTime) {
this.equities = equities;
this.handranks = handranks;
this.iterations = iterations;
this.elapsedTime = elapsedTime;
}
static calculate(cardgroups, board, gameVariant, iterations) {
if (board && [0, 3, 4, 5].indexOf(board.length) === -1) {
throw new Error('The board must contain 0, 3, 4 or 5 cards');
}
const allGroups = board ? cardgroups.concat(board) : cardgroups;
let allCards = [];
allGroups.forEach((group) => {
allCards = allCards.concat(group);
});
// Invalid card values
if (gameVariant === 'short') {
allCards.forEach((card) => {
if (card.getRank() < 6) {
throw new Error('Only cards rank 6 through A are valid.');
}
});
}
const uniqCards = uniqBy(allCards, (card) => {
return card.getRank() + '-' + card.getSuit();
});
if (uniqCards.length !== allCards.length) {
throw new Error('Detected duplicate cards');
}
iterations = iterations || 0;
let game;
if (gameVariant === 'short') {
game = new ShortDeckGame();
}
else {
game = new FullDeckGame();
}
let handranks = [];
// Find out which cards are left in the deck
const remainingCards = new CardGroup();
if (!board || board.length <= 4) {
for (const suit of Suit.all()) {
for (const rank of game.rank.all()) {
const c = new Card(rank, suit);
let isUsed = false;
if (board) {
for (const boardCard of board) {
if (c.equals(boardCard)) {
isUsed = true;
break;
}
}
}
if (!isUsed) {
for (const cardgroup of cardgroups) {
for (const card of cardgroup) {
if (c.equals(card)) {
isUsed = true;
break;
}
}
if (isUsed) {
break;
}
}
}
if (!isUsed) {
remainingCards.push(c);
}
}
}
}
const remainingCount = remainingCards.length;
// Figure out hand ranking
handranks = cardgroups.map((cardgroup) => {
return HandRank.evaluate(game, board ? cardgroup.concat(board) : cardgroup);
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const equities = cardgroups.map((cardgroup) => {
return new HandEquity();
});
const selectWinners = (simulatedBoard) => {
let highestRanking = null;
let highestRankingIndex = [];
for (let i = 0; i < cardgroups.length; i += 1) {
const handranking = HandRank.evaluate(game, cardgroups[i].concat(simulatedBoard));
const isBetter = highestRanking
? handranking.compareTo(highestRanking)
: -1;
if (highestRanking === null || isBetter >= 0) {
if (isBetter === 0) {
highestRankingIndex.push(i);
}
else {
highestRankingIndex = [i];
}
highestRanking = handranking;
}
}
for (let i = 0; i < cardgroups.length; i += 1) {
let isWinning = false;
let isTie = false;
if (highestRankingIndex.length > 1) {
isTie = (highestRankingIndex.indexOf(i) > -1);
}
else {
isWinning = (highestRankingIndex.indexOf(i) > -1);
}
equities[i].addPossibility(isWinning, isTie);
}
};
const jobStartedAt = +new Date();
if (!board || board.length === 0) {
iterations = iterations || OddsCalculator.DEFAULT_ITERATIONS;
for (let x = iterations; x > 0; x -= 1) {
const index1 = random(0, remainingCount - 1);
let index2;
let index3;
let index4;
let index5;
do {
index2 = random(0, remainingCount - 1);
} while (index2 === index1);
do {
index3 = random(0, remainingCount - 1);
} while (index3 === index1 || index3 === index2);
do {
index4 = random(0, remainingCount - 1);
} while (index4 === index1 || index4 === index2 || index4 === index3);
do {
index5 = random(0, remainingCount - 1);
} while (index5 === index1 || index5 === index2 || index5 === index3 || index5 === index4);
const simulatedBoard = CardGroup.fromCards([
remainingCards[index1],
remainingCards[index2],
remainingCards[index3],
remainingCards[index4],
remainingCards[index5]
]);
selectWinners(simulatedBoard);
}
}
else if (board.length >= 5) {
iterations = 1;
selectWinners(board);
}
else if (board.length === 4) {
for (const c of remainingCards) {
const simulatedBoard = board.concat(CardGroup.fromCards([c]));
iterations += 1;
selectWinners(simulatedBoard);
}
}
else if (board.length === 3) {
for (let a = 0; a < remainingCount; a += 1) {
for (let b = a + 1; b < remainingCount; b += 1) {
const simulatedBoard = board.concat(CardGroup.fromCards([remainingCards[a], remainingCards[b]]));
iterations += 1;
selectWinners(simulatedBoard);
}
}
}
const jobEndedAt = +new Date();
return new OddsCalculator(equities, handranks, iterations, jobEndedAt - jobStartedAt);
}
getIterationCount() {
return this.iterations;
}
getElapsedTime() {
return this.elapsedTime;
}
getHandRank(index) {
return this.handranks[index];
}
}
OddsCalculator.DEFAULT_ITERATIONS = 100000;
//# sourceMappingURL=OddsCalculator.js.map