holdem v0.1.0
holdem
A fast implementation of Texas Hold'em poker odds calculator and its related data models.
- 🏃♀️ Blazing fast odds evaluation
- ⚙️ Modern TypeScript interface and providing useful helpers
- 💯 Written in 100% pure universal TypeScript - run on both Node and browsers
- ✅ Fully tested
API
Rank
A enum-like representation of rank in playing cards.
Rank.Ace; // => A of playing cards
Rank.Deuce; // => 2 of playing cardsRank#parse(char: string): Rank
Parses a char (= 1-charactor-length string) into a Rank.
Rank.parse("A") === Rank.Ace;
Rank.parse("T") === Rank.Ten;
Rank.parse("5") === Rank.Five;Only "A", "K", "Q", "J", "T", "9", "8", "7", "6", "5", "4", "3" or "2" is acceptable.
Rank.parse("a"); // => Error: "a" is not a valid string value for Rank.parse().For consistency reason, it needs to be a string even for number-based ranks (e.g. "4").
Rank.parse("4") === Rank.Four;
Rank.parse(4); // => Error: 4 is not a valid string value for Rank.parse().Rank#compare(other: Rank): number
Compares two ranks in power order and returns integer compatible with Array#sort().
Rank.Ace.compare(Rank.King); // => negative integer
Rank.King.compare(Rank.Ace); // => positive integer
Rank.Ace.compare(Rank.Ace); // => 0
Rank.Ace.compare(Rank.Deuce); // => positive integerRank#format(): string
Returns a char for the Rank. The returning string is compatible for Rank.parse().
Rank.Ace.format() === "A";
Rank.Ten.format() === "T";
Rank.Five.format() === "5";Suit
A enum-like representation of suit in playing cards.
Suit.Spade; // => spade of playing cards
Suit.Diamond; // => diamond of playing cardsSuit.parse(char: string): Suit
Parses a char (= 1-charactor-length string) into a Suit.
Suit.parse("s") === "s";
Suit.parse("h") === "h";
Suit.parse("d") === "d";
Suit.parse("c") === "c";Only "s", "h", "d", or "c" is acceptable.
Suit.parse("S"); // => Error: "S" is not a valid string value for Suit.parse().Suit#compare(other: Suit): number
Compares two ranks in index order and returns integer compatible with Array#sort().
It results in the ordinal order that how it should usually be in poker.
Suit.Spade.compare(Suit.Heart); // => negative integer
Suit.Club.compare(Suit.Diamond); // => positive integer
Suit.Heart.compare(Suit.Heart); // => 0Suit#format(): string
Returns a char for the Suit. The returning string is compatible for Suit.parse().
Suit.Spade.format() === "s";
Suit.Diamond.format() === "d";Card
A class representing a piece of playing cards.
Card.parse(expression: string): Card
Parses a string into a Card.
Card.parse("As").equals(new Card(Rank.Ace, Suit.Spade)); // => true
Card.parse("2s").equals(new Card(Rank.Deuce, Suit.Spade)); // => true
Card.parse("Kc").equals(new Card(Rank.King, Suit.Club)); // => truenew Card(rank: Rank, suit: Suit): Card
Creates a Card from a given pair of Rank and Suit.
new Card(Rank.Ace, Suit.Spade);Card#rank: Rank
Rank of the Card.
new Card(Rank.Ace, Suit.Spade).rank === Rank.Ace; // => true
new Card(Rank.Trey, Suit.Heart).rank === Rank.Trey; // => true
new Card(Rank.Ten, Suit.Diamond).rank === Rank.Ten; // => true
new Card(Rank.Six, Suit.Club).rank === Rank.Six; // => trueCard#suit: Suit
Suit of the Card.
new Card(Rank.Ace, Suit.Spade).suit === Suit.Spade; // => true
new Card(Rank.Trey, Suit.Heart).suit === Suit.Heart; // => true
new Card(Rank.Ten, Suit.Diamond).suit === Suit.Diamond; // => true
new Card(Rank.Six, Suit.Club).suit === Suit.Club; // => trueCard#compare(other: Card): number
Compares two cards in power order and returns integer compatible with Array#sort().
new Card(Rank.Ace, Suit.Spade).compare(new Card(Rank.Ace, Suit.Diamond)); // => negative integer
new Card(Rank.Ace, Suit.Diamond).compare(new Card(Rank.Six, Suit.Heart)); // => negative integer
new Card(Rank.Queen, Suit.Spade).compare(new Card(Rank.Ace, Suit.Heart)); // => positive integer
new Card(Rank.Ace, Suit.Club).compare(new Card(Rank.Ace, Suit.Club)); // => 0Card#equals(other: Card): boolean
Whether the given card has the same rank and suit or not.
new Card(Rank.Ace, Suit.Spade).equals(new Card(Rank.Ace, Suit.Spade)); // => true
new Card(Rank.Ace, Suit.Spade).equals(new Card(Rank.Ace, Suit.Diamond)); // => false
new Card(Rank.Ace, Suit.Spade).equals(new Card(Rank.Deuce, Suit.Spade)); // => falseCard#format(): string
Stringifies a Card.
new Card(Rank.Ace, Suit.Spade).format() === "As";
new Card(Rank.Deuce, Suit.Spade).format() === "2s";
new Card(Rank.King, Suit.Club).format() === "Kc";CardSet
Immutable and performant set of Card(s).
CardSet.empty(): CardSet
An empty CardSet.
CardSet.full(): CardSet
A CardSet that has all the cards.
CardSet.from(cards: Iterable<Card>): CardSet
Creates a CardSet from Cards.
const cardSet = CardSet.from([
new Card(Rank.Ace, Suit.Spade),
new Card(Rank.King, Rank.Club),
new Card(Rank.Queen, Rank.Queen),
new Card(Rank.Jack, Rank.Diamond),
]);CardSet.parse(expression: string): CardSet
Parses a string into CardSet.
CardSetUtils.parse("AsQhJdKc") === CardSet.from([
new Card(Rank.Ace, Suit.Spade),
new Card(Rank.King, Rank.Club),
new Card(Rank.Queen, Rank.Queen),
new Card(Rank.Jack, Rank.Diamond),
]); // => true
CardSetUtils.parse("") === CardSetUtils.empty; // => trueCardSet#size: number
Returns number of cards in a CardSet.
const cardSet = CardSet.from([
new Card(Rank.Ace, Suit.Spade),
new Card(Rank.King, Rank.Club),
new Card(Rank.Queen, Rank.Queen),
new Card(Rank.Jack, Rank.Diamond),
]);
cardSet.size; // => 4
CardSet.empty().size; // => 0
CardSet.full().size; // => 52CardSet#has(other: Card | CardSet): boolean
Returns whether a CardSet contains a Card or all the Card in another CardSet.
const cardSet = CardSet.from([
new Card(Rank.Ace, Suit.Spade),
new Card(Rank.King, Suit.Club),
new Card(Rank.Queen, Suit.Heart),
new Card(Rank.Jack, Suit.Diamond),
]);
cardSet.has(CardSet.from([
new Card(Rank.Ace, Suit.Spade),
new Card(Rank.Queen, Suit.Heart),
])); // => true
cardSet.has(CardSet.from([
new Card(Rank.Ace, Suit.Spade),
new Card(Rank.Ace, Suit.Heart),
])); // => false
cardSet.has(new Card(Rank.Ace, Suit.Spade)); // => true
cardSet.has(CardSet.empty()); // => trueCardSet#at(index: number): Card | null
Returns Card at the index.
const cardSet = CardSet.from([
new Card(Rank.Ace, Suit.Spade),
new Card(Rank.King, Suit.Club),
new Card(Rank.Queen, Suit.Heart),
new Card(Rank.Jack, Suit.Diamond),
]);
cardSet.at(2); // => Card<Jd>CardSet#added(other: Card | CardSet): CardSet
Returns an union of CardSet(s).
CardSetUtils.union(
CardSet.from([
new Card(Rank.Ace, Suit.Spade),
new Card(Rank.Queen, Rank.Queen),
]),
CardSet.from([
new Card(Rank.King, Rank.Club),
new Card(Rank.Jack, Rank.Diamond),
]),
) === CardSet.from([
new Card(Rank.Ace, Suit.Spade),
new Card(Rank.King, Rank.Club),
new Card(Rank.Queen, Rank.Queen),
new Card(Rank.Jack, Rank.Diamond),
]);CardSet#removed(other: Card | CardSet): CardSet
Returns a difference (=relative complement) of CardSet(s).
CardSet.from([
new Card(Rank.Ace, Suit.Spade),
new Card(Rank.Queen, Rank.Queen),
]).union(
CardSet.from([
new Card(Rank.King, Rank.Club),
new Card(Rank.Jack, Rank.Diamond),
]),
).equals(
CardSet.from([
new Card(Rank.Ace, Suit.Spade),
new Card(Rank.King, Rank.Club),
new Card(Rank.Queen, Rank.Queen),
new Card(Rank.Jack, Rank.Diamond),
])
); // => trueCardSet#format(): string
Stringifies a CardSet.
CardSet.from([
new Card(Rank.Ace, Suit.Spade),
new Card(Rank.Queen, Rank.Queen),
new Card(Rank.Jack, Rank.Diamond),
new Card(Rank.King, Rank.Club),
]).format() === "AsQhJdKc";HandRange
An immutable map class that represents a set of CardPair(s) and their existance probability.
As HandRange is iterable of [CardSet, number], you can use this in for loop.
for (const [cardSet, probability] of HandRange.parse("88-66")) {
// ...
}HandRange.empty(): HandRange
Returns an empty HandRange.
const handRange = HandRange.empty();HandRange.parse(expression: string): HandRange
Parses a string and returns a HandRange.
The parameter string needs to be comma-separated parts. Each parts should be <hand>:<probability> where <hand> is the following specifiers and <probability> is float.
AsKc- specific pair of cards.66- all pocket pair combinations of six.AKs- all suited pair combinations of ace and king.T9o- all ofsuit pair combinations of ten and nine.JJ-99- all pocket combinations ofJJ,TTand99.86s-84s- all suited combinations of86s,85sand84s.AJo-A9o- all ofsuite combinations ofAJo,AToandA9o.JJ+- equivalent toAA-JJ.85s+- equivalent to87s-85s.AQo+- equivalent toAKo-AQo.
The probability can be omitted. AsKs works as same as AsKs:1.
const handRange = HandRange.parse("88-66:0.66,JJ+:0.5,44,AQs-A9s:0.2");HandRange.from(cardSets: Iterable<CardSet>): HandRange
Returns a HandRange that contains all the given entries of CardSet and its existance probability.
const handRange = HandRange.from([
CardSet.parse("AsKs"),
CardSet.parse("8h8d"),
CardSet.parse("6s5s"),
]);HandRange#size: number
Number of entries in the HandRange.
const handRange = HandRange.parse("88-66");
handRange.size; // => 18HandRange#get(cardSet: CardSet): number | null
Returns probability for the given CardSet.
const handRange = HandRange.parse("88:1,77:0.5,66:0.25");
const probability = handRange.get(CardSet.parse("7s7d"));HandRange#added(other: HandRange): HandRange
Returns an union of the HandRange and the given one.
const handRange = HandRange.parse("88-66");
const ninesAdded = handRange.added(HandRange.parse("99"));HandRange#removed(other: HandRange): HandRange
Returns a new HandRange that the given hand range removed.
const handRange = HandRange.parse("88-66");
const sevensRemoved = handRange.removed(HandRange.parse("77"));HandRange#onlyRankPairs(): Map<string, number>
Returns a map of rank pair string and its probability. Map key is string representation of rank pair such as AsKh. Detatched card pairs are ignored.
const handRange = HandRange.parse("AKs:0.5");
const onlyRankPairs = handRange.onlyRankPairs();
onlyRankPairs.entries(); // => Iterable of ["AsKs", 0.5], ["AhKh", 0.5], ...HandRange#format(): string
Returns formatted string representation of the HandRange. Each card pairs will be merged and grouped into rank pairs.
const handRange = HandRange.parse("AsKs:1,AhKh:1,AdKd:1,AcKc:1");
handRange.format(); // => "AKs:1"Evaluator
A class that evaluates equity of each player in a certain situation.
You can use either of these implementations:
new MontecarloEvaluator()- randomly decides the possible situations can come and evaluate equities.new ExhaustiveEvaluator()- exhaustively iterates all the possible situations and evaluate equities.
Evaluator implements Iterable<Matchup> so you can use it in for-loop.
const evaluator = new ExhaustiveEvaluator({
board: CardSet.parse("AsKcQh2d"),
players: [
HandRange.parse("KdJd"),
HandRange.parse("Ah3h"),
],
});
for (const matchup of evaluator) {
// ...
}Evaluator#board: CardSet
The initial board cards for the situation.
Evaluator#players: HandRange[]
The HandRange(s) that the players have.
MontecarloEvaluator
An Evaluator that does montecarlo simulation for certain times.
Do not forget to call #take() because this evaluator can run unlimited times.
const evaluator = new MontecarloEvaluator({
board: CardSet.parse("AsKcQh2d"),
players: [
HandRange.parse("KdJd"),
HandRange.parse("Ah3h"),
],
});
for (const matchup of evaluator.take(10000)) {
// ...
}MontecarloEvaluator#take(times: number): Iterable<Matchup>
Limits times to run and returns Iterable<Matchup>.
const evaluator = new MontecarloEvaluator({
board: CardSet.parse("AsKcQh2d"),
players: [
HandRange.parse("KdJd"),
HandRange.parse("Ah3h"),
],
});
for (const matchup of evaluator.take(10000)) {
// ...
}ExhaustiveEvaluator
An Evaluator that exhaustively simulates all the possible situations. This is suitable for the situation that has only small number of possible futures.
const evaluator = new ExhaustiveEvaluator({
board: CardSet.parse("AsKcQh2d"),
players: [
HandRange.parse("KdJd"),
HandRange.parse("Ah3h"),
],
});
for (const matchup of evaluator) {
// ...
}Matchup
The eventual situation of the evaluation and its result.
Matchup#board: CardSet
The eventual board card in the situation.
Matchup#players: { cards: CardSet; hand: MadeHand; win: boolean }[]
Result of each player in the situation.
Matchup#wonPlayerCount: number
Number of the players won the pot.
MadeHand
The final hand that player made at showdown.
MadeHand#powerIndex: number
The power index of the MadeHand. 0 is the strongest (the top straight flush), 7462 is the worst trash hand.
MadeHand#type: "highcard" | "pair" | "two-pairs" | "trips" | "straight" | "flush" | "full-house" | "quads" | "straight-flush"
The type of MadeHand.
Contributing
Please folk and clone the repository and run npm install. Make a pull request towards main branch in this repository.