Skip to content

Commit

Permalink
fixup! [WIP] update odds function to use bit masks
Browse files Browse the repository at this point in the history
  • Loading branch information
mhuggins committed May 13, 2024
1 parent 74bbcea commit c6b4c50
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 71 deletions.
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,22 +340,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
21 changes: 11 additions & 10 deletions src/__tests__/odds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,26 +49,27 @@ describe('odds', () => {
});

// TODO: this test would have taken forever previously, does it run quickly now?
it.skip('multi-way, all hole cards provided without community cards', () => {
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: 58, ties: 228, total: 1806 },
{ wins: 56, ties: 228, total: 1806 },
{ wins: 1464, ties: 0, total: 1806 },
{ 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.skip('heads-up, not all hole cards provided', () => {
it('heads-up, not all hole cards provided', () => {
const hands: Hand[] = [['As', 'Ks'], ['Ad']];

// TODO: these equity values does not add up to 100%!!
expect(odds(hands, { ...holdemOptions, communityCards: ['Qd', 'Js', '8h'] })).toEqual([
{ wins: 29536, ties: 4136, total: 45540, equity: 0.6939833114 },
{ wins: 11868, ties: 4136, total: 45540, equity: 0.3060166886 },
{ wins: 29536, ties: 4136, total: 45540, equity: 0.6490040470283204 },
{ wins: 11868, ties: 4136, total: 45540, equity: 0.2610290861911965 },
]);
});

Expand Down Expand Up @@ -96,15 +97,15 @@ describe('odds', () => {
maximumHoleCardsUsed: 7,
};

it.skip('heads-up (no ties)', () => {
it('heads-up (no ties)', () => {
const hands: Hand[] = [
['As', 'Kd', 'Ks', '8c', 'Ac', '2d'],
['9s', '8s', 'Ts', '6s', '4h', '2c'],
];

expect(odds(hands, studOptions)).toEqual([
{ wins: 603, ties: 0, total: 780 },
{ wins: 177, ties: 0, total: 780 },
{ wins: 1206, ties: 0, total: 1560, equity: 0.7735727098575103 },
{ wins: 354, ties: 0, total: 1560, equity: 0.2270687272250464 },
]);
});
});
Expand Down
125 changes: 80 additions & 45 deletions src/odds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,66 +13,101 @@ export interface OddsOptions {
maximumHoleCardsUsed: number;
}

function* iterateHoleCardMasks(
allHoleCardMasks: bigint[],
deadCardsMask: bigint,
expectedHoleCardCount: number,
): Generator<bigint[], void> {
if (allHoleCardMasks.length === 0) {
return;
}

const [holeCardsMask, ...allRemainingHoleCardMasks] = allHoleCardMasks;

const cardMasks = iterateCardMasks(holeCardsMask, deadCardsMask, expectedHoleCardCount);

if (allRemainingHoleCardMasks.length > 0) {
for (const cardMask of cardMasks) {
for (const remainingHoleCardMasks of iterateHoleCardMasks(
allRemainingHoleCardMasks,
cardMask | deadCardsMask,
expectedHoleCardCount,
)) {
yield [cardMask, ...remainingHoleCardMasks];
}
}
} else {
for (const cardMask of cardMasks) {
yield [cardMask];
}
}
}

export const odds = (allHoleCards: Hand[], options: OddsOptions): Odds[] => {
let total = 0;

const odds: Odds[] = allHoleCards.map(() => ({ wins: 0, ties: 0, total: 0, equity: 0 }));

const allDeadCardsMask = [
const initialDeadCardsMask = getHandMask([
...allHoleCards.reduce((acc, cards) => [...acc, ...cards]),
...options.communityCards,
...(options.deadCards ?? []),
];

// TODO: We're not using options.minimumHoleCardsUsed, options.maximumHoleCardsUsed,
// options.expectedHoleCardCount, or ~options.expectedCommunityCardCount~.
// TODO: This only iterates community cards, which won't work for games like stud where
// there are no community cards (i.e.: cards must be added to players' hands).
// Resolving both of these TODO items will require having a for-loop similar to the
// boardMasks loop over `iterateCardMasks`, but one that iterates over `allHoleCards`,
// which in turn calls `iterateCardMasks` as well. We'll basically need to construct
// masks based upon the hole cards that are selected to be iterated over.
const boardMasks = iterateCardMasks(
getHandMask(options.communityCards),
getHandMask(allDeadCardsMask),
options.expectedCommunityCardCount,
]);

const allHoleCardMasksIterator = iterateHoleCardMasks(
allHoleCards.map(getHandMask),
initialDeadCardsMask,
options.expectedHoleCardCount,
);

let total = 0;
for (const allHoleCardMasks of allHoleCardMasksIterator) {
const currentDeadCardsMask = allHoleCardMasks.reduce(
(acc, mask) => acc | mask,
initialDeadCardsMask,
);

for (const boardMask of boardMasks) {
let handMask = getHandMask(allHoleCards[0]) | boardMask;
let bestHandValue = getHandValueMask(handMask);
let bestHandIndices = new Set([0]);

for (let i = 1; i < allHoleCards.length; i += 1) {
handMask = getHandMask(allHoleCards[i]) | boardMask;
const currentHandValue = getHandValueMask(handMask);
if (currentHandValue > bestHandValue) {
bestHandValue = currentHandValue;
bestHandIndices = new Set([i]);
} else if (currentHandValue === bestHandValue) {
bestHandIndices.add(i);
const boardMasks = iterateCardMasks(
getHandMask(options.communityCards),
currentDeadCardsMask,
options.expectedCommunityCardCount,
);

for (const boardMask of boardMasks) {
let handMask = allHoleCardMasks[0] | boardMask;
let bestHandValue = getHandValueMask(handMask);
let bestHandIndices = new Set([0]);

for (let i = 1; i < allHoleCardMasks.length; i += 1) {
handMask = allHoleCardMasks[i] | boardMask;
const currentHandValue = getHandValueMask(handMask);
if (currentHandValue > bestHandValue) {
bestHandValue = currentHandValue;
bestHandIndices = new Set([i]);
} else if (currentHandValue === bestHandValue) {
bestHandIndices.add(i);
}
}
}

const isTie = bestHandIndices.size > 1;
const equity = isTie ? 1 / bestHandIndices.size : 1;
for (let i = 0; i < allHoleCards.length; i += 1) {
if (bestHandIndices.has(i)) {
if (isTie) {
odds[i].ties += 1;
odds[i].equity += equity;
} else {
odds[i].wins += 1;
const isTie = bestHandIndices.size > 1;
const equity = isTie ? 1 / bestHandIndices.size : 1;
for (let i = 0; i < allHoleCardMasks.length; i += 1) {
if (bestHandIndices.has(i)) {
if (isTie) {
odds[i].ties += 1;
odds[i].equity += equity;
} else {
odds[i].wins += 1;
}
}
}
}

total += 1;
}
total += 1;
}

for (let i = 0; i < allHoleCards.length; i += 1) {
odds[i].total = total;
odds[i].equity = (odds[i].wins + odds[i].equity) / odds[i].total;
for (let i = 0; i < allHoleCardMasks.length; i += 1) {
odds[i].total = total;
odds[i].equity = (odds[i].wins + odds[i].equity) / odds[i].total;
}
}

return odds;
Expand Down

0 comments on commit c6b4c50

Please sign in to comment.