Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 21 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ A collection of useful functions for determining the strongest possible hand giv

The following types are defined & utilized by this package.

- `EvaluatedHand`: An object representing the effective hand & strength, given a coordination of cards.
- `Odds`: An object representing how a hand will perform given a scenario. Includes the number of `wins`, `ties`, and `total` possible outcomes.
- `EvaluatedHand`: An object representing the effective hand, strength, and calculated value, given a coordination of cards.
- `Odds`: An object representing how a hand will perform given a scenario. Includes the number of `wins`, `ties`, and `total` possible outcomes, as well as an `equity` percentage between 0-1.

### Core Functions

Expand Down Expand Up @@ -118,16 +118,13 @@ const generate = simulate({
maximumHoleCardsUsed: 2,
});

let result = generate.next();
while (!result.done) {
for (const result of generate) {
const hand1WinPercent = ((result[0].wins / result[0].total) * 100).toFixed(1);

// Output the cumulative results every 500 runs.
if (result[0].total % 500 === 0) {
console.log(hand1WinPercent, result);
}

result = generate.next();
}

// => "13.8" [{ wins: 69, ties: 0, total: 500 }, { wins: 431, ties: 0, total: 500 }]
Expand Down Expand Up @@ -158,8 +155,8 @@ const result = odds([hand1, hand2], {

console.log(result);
// => [
// { wins: 304, ties: 0, total: 1980 },
// { wins: 1676, ties: 0, total: 1980 },
// { wins: 304, ties: 0, total: 1980, equity: 0.1535353535 },
// { wins: 1676, ties: 0, total: 1980, equity: 0.8464646465 },
// ]
```

Expand Down Expand Up @@ -340,22 +337,22 @@ Benchmarked on an Apple M1 MacBook Pro (2020) with 16 GB RAM using macOS Sonoma
┌─────────┬─────────────────────────────────┬───────────┬────────────────────┬──────────┬─────────┐
│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │
├─────────┼─────────────────────────────────┼───────────┼────────────────────┼──────────┼─────────┤
│ 0 │ 'evaluate high card' │ '355,417' │ 2813.5928118446004 │ '±1.07%' │ 177709
│ 1 │ 'evaluate one pair' │ '338,217' │ 2956.6739913315823 │ '±1.22%' │ 169109
│ 2 │ 'evaluate two pair' │ '332,969' │ 3003.276373247081 │ '±1.37%' │ 166485
│ 3 │ 'evaluate three of a kind' │ '356,214' │ 2807.2995373597214 │ '±1.39%' │ 178108
│ 4 │ 'evaluate straight' │ '293,999' │ 3401.3635219867597 │ '±1.27%' │ 147042
│ 5 │ 'evaluate flush' │ '234,729' │ 4260.22771695116 │ '±2.24%' │ 117365
│ 6 │ 'evaluate full house' │ '445,363' │ 2245.3578151797146 │ '±1.44%' │ 222682
│ 7 │ 'evaluate four of a kind' │ '435,199' │ 2297.794512868328 │ '±1.54%' │ 217600
│ 8 │ 'evaluate straight flush' │ '379,529' │ 2634.8410718516025 │ '±1.13%' │ 189765
│ 9 │ 'evaluate royal flush' │ '341,783' │ 2925.830758606707 │ '±1.42%' │ 170892
│ 10 │ 'odds holdem heads up to flop' │ '60' │ 16660399.096774053 │ '±0.33%' │ 31
│ 11 │ 'odds holdem heads up to turn' │ '2,649' │ 377453.67849057174 │ '±2.17%' │ 1325
│ 12 │ 'odds holdem heads up to river' │ '107,341' │ 9316.064299157219 │ '±1.50%' │ 53671
│ 13 │ 'odds holdem multiway to flop' │ '39' │ 25622593.799999684 │ '±0.21%' │ 20
│ 14 │ 'odds holdem multiway to turn' │ '1,597' │ 625997.5319148765 │ '±2.03%' │ 799
│ 15 │ 'odds holdem multiway to river' │ '59,267' │ 16872.57771478776 │ '±1.56%' │ 29634
│ 0 │ 'evaluate high card' │ '476,487' │ 2098.6921895201767 │ '±1.18%' │ 238244
│ 1 │ 'evaluate one pair' │ '470,466' │ 2125.5510130339558 │ '±1.13%' │ 235234
│ 2 │ 'evaluate two pair' │ '478,362' │ 2090.4626853186687 │ '±1.14%' │ 239182
│ 3 │ 'evaluate three of a kind' │ '453,772' │ 2203.7472354081356 │ '±1.34%' │ 226887
│ 4 │ 'evaluate straight' │ '506,457' │ 1974.5004993935945 │ '±1.28%' │ 253307
│ 5 │ 'evaluate flush' │ '463,446' │ 2157.744307884392 │ '±1.20%' │ 231724
│ 6 │ 'evaluate full house' │ '485,895' │ 2058.054941403109 │ '±1.78%' │ 243787
│ 7 │ 'evaluate four of a kind' │ '493,954' │ 2024.4764391983213 │ '±1.29%' │ 246978
│ 8 │ 'evaluate straight flush' │ '543,500' │ 1839.9232459130653 │ '±1.53%' │ 271751
│ 9 │ 'evaluate royal flush' │ '534,632' │ 1870.443428589219 │ '±1.45%' │ 267317
│ 10 │ 'odds holdem heads up to flop' │ '928' │ 1076827.0537634292 │ '±1.10%' │ 465
│ 11 │ 'odds holdem heads up to turn' │ '19,546' │ 51159.82453448301 │ '±1.36%' │ 9774
│ 12 │ 'odds holdem heads up to river' │ '321,114' │ 3114.156684814034 │ '±1.41%' │ 160558
│ 13 │ 'odds holdem multiway to flop' │ '583' │ 1715193.0547944885 │ '±1.03%' │ 292
│ 14 │ 'odds holdem multiway to turn' │ '11,435' │ 87445.18188177259 │ '±1.20%' │ 5718
│ 15 │ 'odds holdem multiway to river' │ '181,551' │ 5508.093615051668 │ '±1.43%' │ 90776
└─────────┴─────────────────────────────────┴───────────┴────────────────────┴──────────┴─────────┘
```

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"prepublishOnly": "yarn clean && yarn lint && yarn format && yarn test && yarn build"
},
"dependencies": {
"@poker-apprentice/types": "^1.4.0",
"@poker-apprentice/types": "^1.4.2",
"assert-never": "^1.2.1",
"lodash": "^4.17.21",
"v8-profiler-next": "^1.10.0"
},
Expand Down
7 changes: 7 additions & 0 deletions src/__tests__/evaluate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ describe('evaluate', () => {
});
});

it('recognizes high card', () => {
expect(evaluate({ holeCards: ['As', 'Qd', 'Js', '8s', '2h', '3h'] })).toEqual({
strength: HandStrength.HighCard,
hand: ['As', 'Qd', 'Js', '8s', '3h'],
});
});

describe('minimum & maximum hole cards', () => {
it('handles minimum & maximum as the same value', () => {
const omahaHand = evaluate({
Expand Down
77 changes: 63 additions & 14 deletions src/__tests__/odds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ describe('odds', () => {
];

expect(odds(hands, { ...holdemOptions, communityCards: ['Qd', 'Js', '8d'] })).toEqual([
{ wins: 298, ties: 0, total: 1980 },
{ wins: 1682, ties: 0, total: 1980 },
{ wins: 149, ties: 0, total: 990, equity: 0.1505050505050505 },
{ wins: 841, ties: 0, total: 990, equity: 0.8494949494949495 },
]);
});

Expand All @@ -29,31 +29,45 @@ describe('odds', () => {
];

expect(odds(hands, { ...holdemOptions, communityCards: ['Qd', 'Js', '8h'] })).toEqual([
{ wins: 90, ties: 1800, total: 1980 },
{ wins: 90, ties: 1800, total: 1980 },
{ wins: 45, ties: 900, total: 990, equity: 0.5 },
{ wins: 45, ties: 900, total: 990, equity: 0.5 },
]);
});

it('multi-way, all hole cards provided', () => {
it('multi-way, all hole cards provided with community cards', () => {
const hands: Hand[] = [
['As', 'Ks'],
['Ad', 'Kd'],
['Jd', 'Jh'],
];

expect(odds(hands, { ...holdemOptions, communityCards: ['Qd', 'Js', '8h'] })).toEqual([
{ wins: 58, ties: 228, total: 1806 },
{ wins: 56, ties: 228, total: 1806 },
{ wins: 1464, ties: 0, total: 1806 },
{ wins: 29, ties: 114, total: 903, equity: 0.09523809523809523 },
{ wins: 28, ties: 114, total: 903, equity: 0.09413067552602436 },
{ wins: 732, ties: 0, total: 903, equity: 0.8106312292358804 },
]);
});

it('multi-way, all hole cards provided without community cards', () => {
const hands: Hand[] = [
['As', 'Ks'],
['Ad', 'Kd'],
['Jd', 'Jh'],
];

expect(odds(hands, { ...holdemOptions, communityCards: [] })).toEqual([
{ wins: 96209, ties: 405190, total: 1370754, equity: 0.21729366951814816 },
{ wins: 79965, ties: 405190, total: 1370754, equity: 0.20544325726328697 },
{ wins: 789390, ties: 5687, total: 1370754, equity: 0.5772630732185837 },
]);
});

it('heads-up, not all hole cards provided', () => {
const hands: Hand[] = [['As', 'Ks'], ['Ad']];

expect(odds(hands, { ...holdemOptions, communityCards: ['Qd', 'Js', '8h'] })).toEqual([
{ wins: 59072, ties: 8272, total: 91080 },
{ wins: 23736, ties: 8272, total: 91080 },
{ wins: 29536, ties: 4136, total: 45540, equity: 0.6939833113746158 },
{ wins: 11868, ties: 4136, total: 45540, equity: 0.3060166886253843 },
]);
});

Expand All @@ -66,8 +80,8 @@ describe('odds', () => {
expect(
odds(hands, { ...holdemOptions, communityCards: ['Qd', 'Js', '8h', 'Th', '2s'] }),
).toEqual([
{ wins: 1, ties: 0, total: 1 },
{ wins: 0, ties: 0, total: 1 },
{ wins: 1, ties: 0, total: 1, equity: 1 },
{ wins: 0, ties: 0, total: 1, equity: 0 },
]);
});
});
Expand All @@ -88,8 +102,43 @@ describe('odds', () => {
];

expect(odds(hands, studOptions)).toEqual([
{ wins: 1206, ties: 0, total: 1560 },
{ wins: 354, ties: 0, total: 1560 },
{ wins: 1206, ties: 0, total: 1560, equity: 0.7730769230769231 },
{ wins: 354, ties: 0, total: 1560, equity: 0.22692307692307692 },
]);
});
});

describe('game requires a specific number of hole cards to be used', () => {
const omahaOptions = {
communityCards: [],
expectedCommunityCardCount: 5,
expectedHoleCardCount: 4,
minimumHoleCardsUsed: 2,
maximumHoleCardsUsed: 2,
};

it('heads-up, all hole cards provided', () => {
const hands: Hand[] = [
['As', 'Ks', 'Kh', 'Tc'],
['Ad', 'Kd', 'Qc', 'Jc'],
];

expect(odds(hands, { ...omahaOptions, communityCards: ['Qd', 'Js', '8h'] })).toEqual([
{ wins: 15220, ties: 2811, total: 35640, equity: 0.46648428731762065 },
{ wins: 17609, ties: 2811, total: 35640, equity: 0.5335157126823793 },
]);
});

// TODO: enable this once implementation works
it.skip('heads-up, all hole cards provided without community cards', () => {
const hands: Hand[] = [
['As', 'Ks', 'Kh', 'Tc'],
['Ad', 'Kd', 'Qc', 'Jc'],
];

expect(odds(hands, { ...omahaOptions, communityCards: [] })).toEqual([
{ wins: 45, ties: 900, total: 990, equity: 0.5 },
{ wins: 45, ties: 900, total: 990, equity: 0.5 },
]);
});
});
Expand Down
13 changes: 10 additions & 3 deletions src/__tests__/simulate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@ describe('simulate', () => {

it('yields results the correct number of times', () => {
const generate = simulate(options);
expect(generate.next().value[0].total).toEqual(1);
expect(generate.next().value[0].total).toEqual(2);
expect(generate.next().value[0].total).toEqual(3);
for (let i = 0; i < 3; i += 1) {
const { value: odds } = generate.next();
expect(odds[0].total).toEqual(i + 1);
}
});

it('calculates equities correctly', () => {
for (const odds of simulate(options)) {
expect(odds.reduce((acc, current) => acc + current.equity, 0)).toBeCloseTo(1);
}
});

it('returns after yielding the maximum number of possible times', () => {
Expand Down
61 changes: 61 additions & 0 deletions src/constants/bitmasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { HandStrength, Rank, Suit } from '@poker-apprentice/types';

const SUIT_CLUBS = 0n;
const SUIT_DIAMONDS = 1n;
const SUIT_HEARTS = 2n;
const SUIT_SPADES = 3n;

export const MASK_OFFSET_CLUBS = 13n * SUIT_CLUBS;
export const MASK_OFFSET_DIAMONDS = 13n * SUIT_DIAMONDS;
export const MASK_OFFSET_HEARTS = 13n * SUIT_HEARTS;
export const MASK_OFFSET_SPADES = 13n * SUIT_SPADES;

export const RANK_MASK = 0b1111111111111n;

export const CARD_BIT_WIDTH = 4n;
export const CARD_5_BIT_SHIFT = 0n;
export const CARD_4_BIT_SHIFT = CARD_BIT_WIDTH + CARD_5_BIT_SHIFT; // 4n
export const CARD_3_BIT_SHIFT = CARD_BIT_WIDTH + CARD_4_BIT_SHIFT; // 8n
export const CARD_2_BIT_SHIFT = CARD_BIT_WIDTH + CARD_3_BIT_SHIFT; // 12n
export const CARD_1_BIT_SHIFT = CARD_BIT_WIDTH + CARD_2_BIT_SHIFT; // 16n
export const HAND_MASK_BIT_SHIFT = 24n;

export const CARD_MASK = 0x0fn;
export const CARD_1_MASK = 0x000f0000n;
export const CARD_2_MASK = 0x0000f000n;
export const CARD_3_MASK = 0x00000f00n;
export const CARD_4_MASK = 0x000000f0n;
export const CARD_5_MASK = 0x0000000fn;

export const HAND_MASK_HIGH_CARD = BigInt(HandStrength.HighCard) << HAND_MASK_BIT_SHIFT;
export const HAND_MASK_ONE_PAIR = BigInt(HandStrength.OnePair) << HAND_MASK_BIT_SHIFT;
export const HAND_MASK_TWO_PAIR = BigInt(HandStrength.TwoPair) << HAND_MASK_BIT_SHIFT;
export const HAND_MASK_THREE_OF_A_KIND = BigInt(HandStrength.ThreeOfAKind) << HAND_MASK_BIT_SHIFT;
export const HAND_MASK_STRAIGHT = BigInt(HandStrength.Straight) << HAND_MASK_BIT_SHIFT;
export const HAND_MASK_FLUSH = BigInt(HandStrength.Flush) << HAND_MASK_BIT_SHIFT;
export const HAND_MASK_FULL_HOUSE = BigInt(HandStrength.FullHouse) << HAND_MASK_BIT_SHIFT;
export const HAND_MASK_FOUR_OF_A_KIND = BigInt(HandStrength.FourOfAKind) << HAND_MASK_BIT_SHIFT;
export const HAND_MASK_STRAIGHT_FLUSH = BigInt(HandStrength.StraightFlush) << HAND_MASK_BIT_SHIFT;

export const RANK_BITS_MAP: Record<Rank, bigint> = {
'2': 0n,
'3': 1n,
'4': 2n,
'5': 3n,
'6': 4n,
'7': 5n,
'8': 6n,
'9': 7n,
T: 8n,
J: 9n,
Q: 10n,
K: 11n,
A: 12n,
};

export const SUIT_BITS_MAP: Record<Suit, bigint> = {
c: 0n,
d: 1n,
h: 2n,
s: 3n,
};
Loading
Loading