Skip to content

Commit

Permalink
determine hand from hand mask & value mask
Browse files Browse the repository at this point in the history
The `hand` property value of `EvaluatedHand` objects returned by the `evaluate`
function now order matching cards by suit alphabetically (c, d, h, s) rather
than the order they appeared in the provided hole card & community card  arrays.

BREAKING CHANGE
  • Loading branch information
mhuggins committed May 5, 2024
1 parent a0f2e21 commit 2caf2dd
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 129 deletions.
14 changes: 7 additions & 7 deletions src/__tests__/evaluate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,23 @@ describe('evaluate', () => {
it('recognizes four of a kind', () => {
expect(evaluate({ holeCards: ['As', 'Qd', 'Js', 'Qs', 'Qc', 'Qh'] })).toEqual({
strength: HandStrength.FourOfAKind,
hand: ['Qd', 'Qs', 'Qc', 'Qh', 'As'],
hand: ['Qc', 'Qd', 'Qh', 'Qs', 'As'],
value: 118145024n,
});
});

it('recognizes full houses', () => {
expect(evaluate({ holeCards: ['As', 'Qd', 'Js', 'Qs', 'Jc', 'Qh'] })).toEqual({
strength: HandStrength.FullHouse,
hand: ['Qd', 'Qs', 'Qh', 'Js', 'Jc'],
hand: ['Qd', 'Qh', 'Qs', 'Jc', 'Js'],
value: 101355520n,
});
});

it('recognizes stronger full houses', () => {
expect(evaluate({ holeCards: ['Js', 'Qd', 'Jc', 'Qs', 'Ac', 'Qh', 'Ah'] })).toEqual({
strength: HandStrength.FullHouse,
hand: ['Qd', 'Qs', 'Qh', 'Ac', 'Ah'],
hand: ['Qd', 'Qh', 'Qs', 'Ac', 'Ah'],
value: 101367808n,
});
});
Expand All @@ -60,7 +60,7 @@ describe('evaluate', () => {
}),
).toEqual({
strength: HandStrength.FullHouse,
hand: ['Kc', 'Kd', 'Kh', '5d', '5c'],
hand: ['Kc', 'Kd', 'Kh', '5c', '5d'],
value: 101396480n,
});
});
Expand Down Expand Up @@ -92,15 +92,15 @@ describe('evaluate', () => {
it('recognizes three of a kind', () => {
expect(evaluate({ holeCards: ['As', 'Qd', 'Js', 'Qs', 'Qc', '2h'] })).toEqual({
strength: HandStrength.ThreeOfAKind,
hand: ['Qd', 'Qs', 'Qc', 'As', 'Js'],
hand: ['Qc', 'Qd', 'Qs', 'As', 'Js'],
value: 51038464n,
});
});

it('recognizes two pair', () => {
expect(evaluate({ holeCards: ['As', 'Qd', 'Js', 'Qs', '2h', 'Jh'] })).toEqual({
strength: HandStrength.TwoPair,
hand: ['Qd', 'Qs', 'Js', 'Jh', 'As'],
hand: ['Qd', 'Qs', 'Jh', 'Js', 'As'],
value: 34249728n,
});
});
Expand Down Expand Up @@ -171,7 +171,7 @@ describe('evaluate', () => {
}),
).toEqual({
strength: HandStrength.OnePair,
hand: ['As', 'Ad'],
hand: ['Ad', 'As'],
value: 17563648n,
});
});
Expand Down
118 changes: 3 additions & 115 deletions src/evaluate.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,9 @@
import { Card, Hand, HandStrength, getRank, getSuit } from '@poker-apprentice/types';
import { assertNever } from 'assert-never';
import findKey from 'lodash/findKey';
import { Card, Hand } from '@poker-apprentice/types';
import { compare } from './compare';
import {
CARD_1_BIT_SHIFT,
CARD_2_BIT_SHIFT,
CARD_3_BIT_SHIFT,
CARD_4_BIT_SHIFT,
CARD_5_BIT_SHIFT,
CARD_MASK,
HAND_MASK_BIT_SHIFT,
} from './constants/bitmasks';
import { CARD_RANK_TABLE } from './constants/cardRankTable';
import { rankOrder } from './constants/rankOrder';
import { EvaluatedHand } from './types';
import { bigintKey } from './utils/bigintKey';
import { evaluateHandMask } from './utils/evaluateHandMask';
import { getCombinations } from './utils/getCombinations';
import { getHandMask } from './utils/getHandMask';
import { getHandValueMask } from './utils/getHandValueMask';
import { getMaskedCardRank } from './utils/getMaskedCardRank';
import { getSuitedRankMasks } from './utils/getSuitedRankMasks';

export interface EvaluateOptions {
holeCards: Card[];
Expand Down Expand Up @@ -97,103 +81,7 @@ const getAllHandCombinations = ({
return allHandCombinations.filter((cards) => cards.length === longestCombination);
};

const take = <T>(array: T[], index: number): T => {
const [item] = array.splice(index, 1);
return item;
};

const constructHand = (
cards: Card[],
cardMasks: bigint[],
maskIndices: [number, number, number, number, number],
): Hand =>
maskIndices.reduce((result: Hand, maskIndex, i) => {
if (maskIndex >= 0) {
const cardMask = cardMasks[maskIndex];
const maskedCardRank = getMaskedCardRank(cardMask);
const cardIndex = cards.findIndex((card) => getRank(card) === maskedCardRank);
const card = take(cards, cardIndex);
if (card !== undefined) {
result.push(card);
}
} else {
const referencedCard = result[i - 1];
const referencedRank = rankOrder.indexOf(getRank(referencedCard));
const cardIndex = cards.findIndex(
(card) => getRank(card) === rankOrder.at((referencedRank + maskIndex + 13) % 13),
);
const card = take(cards, cardIndex);
if (card !== undefined) {
result.push(card);
}
}
return result;
}, []);

const getHand = (
originalCards: Card[],
handMask: bigint,
handValueMask: bigint,
strength: HandStrength,
): Hand => {
const cardMasks = [
(handValueMask >> CARD_1_BIT_SHIFT) & CARD_MASK,
(handValueMask >> CARD_2_BIT_SHIFT) & CARD_MASK,
(handValueMask >> CARD_3_BIT_SHIFT) & CARD_MASK,
(handValueMask >> CARD_4_BIT_SHIFT) & CARD_MASK,
(handValueMask >> CARD_5_BIT_SHIFT) & CARD_MASK,
];

const suits = getSuitedRankMasks(handMask);
const flushSuit = findKey(suits, (v) => CARD_RANK_TABLE[bigintKey(v)] >= 5);
const cards = flushSuit
? originalCards.filter((card) => getSuit(card) === flushSuit)
: originalCards;

switch (strength) {
case HandStrength.HighCard:
return constructHand(cards, cardMasks, [0, 1, 2, 3, 4]);
case HandStrength.OnePair:
return constructHand(cards, cardMasks, [0, 0, 1, 2, 3]);
case HandStrength.TwoPair:
return constructHand(cards, cardMasks, [0, 0, 1, 1, 2]);
case HandStrength.ThreeOfAKind:
return constructHand(cards, cardMasks, [0, 0, 0, 1, 2]);
case HandStrength.Straight:
return constructHand(cards, cardMasks, [0, -1, -1, -1, -1]);
case HandStrength.Flush:
return constructHand(cards, cardMasks, [0, 1, 2, 3, 4]);
case HandStrength.FullHouse:
return constructHand(cards, cardMasks, [0, 0, 0, 1, 1]);
case HandStrength.FourOfAKind:
return constructHand(cards, cardMasks, [0, 0, 0, 0, 1]);
case HandStrength.StraightFlush:
return constructHand(cards, cardMasks, [0, -1, -1, -1, -1]);
case HandStrength.RoyalFlush:
return constructHand(cards, cardMasks, [0, -1, -1, -1, -1]);
default:
return assertNever(strength);
}
};

const getStrength = (handMask: bigint): HandStrength => {
const strength = Number(handMask >> HAND_MASK_BIT_SHIFT);
const highCardRank = getMaskedCardRank((handMask >> CARD_1_BIT_SHIFT) & CARD_MASK);

if (strength === HandStrength.StraightFlush && highCardRank === 'A') {
return HandStrength.RoyalFlush;
}
return strength;
};

const evaluateHand = (cards: Card[]): EvaluatedHand => {
const handMask = getHandMask(cards);
const value = getHandValueMask(handMask);
const strength = getStrength(value);
const hand = getHand(cards, handMask, value, strength);

return { strength, hand, value };
};
const evaluateHand = (hand: Hand) => evaluateHandMask(getHandMask(hand));

export const evaluate = ({
holeCards,
Expand Down
157 changes: 157 additions & 0 deletions src/utils/evaluateHandMask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import {
ALL_SUITS,
Hand,
HandStrength,
Rank,
Suit,
getRank,
isRank,
} from '@poker-apprentice/types';
import { assertNever } from 'assert-never';
import {
CARD_1_BIT_SHIFT,
CARD_2_BIT_SHIFT,
CARD_3_BIT_SHIFT,
CARD_4_BIT_SHIFT,
CARD_5_BIT_SHIFT,
CARD_MASK,
HAND_MASK_BIT_SHIFT,
RANK_BITS_MAP,
} from '../constants/bitmasks';
import { rankOrder } from '../constants/rankOrder';
import { EvaluatedHand } from '../types';
import { findKey } from './findKey';
import { getRankMask } from './getHandMask';
import { getHandValueMask } from './getHandValueMask';
import { getMaskedCardRank } from './getMaskedCardRank';
import { getSuitedRankMasks } from './getSuitedRankMasks';

const constructHand = (
handMask: bigint,
cardMasks: bigint[],
maskIndices: [number, number, number, number, number],
isSuited = false,
): Hand => {
const suits = getSuitedRankMasks(handMask);

const getReferencedRank = (hand: Hand, offset: number) => {
const referencedCard = hand[hand.length - 1];
const referencedRank = rankOrder.indexOf(getRank(referencedCard));
return rankOrder.at((referencedRank + offset + 13) % 13) as Rank;
};

if (isSuited) {
const combinedCardMasks = cardMasks.reduce(
(acc, current) => acc | getRankMask(getMaskedCardRank(current)),
0n,
);
for (const suit of ALL_SUITS) {
const suitedCardsMask = suits[suit];
if ((suitedCardsMask & combinedCardMasks) === combinedCardMasks) {
return maskIndices.reduce((hand: Hand, maskIndex) => {
const rank =
maskIndex >= 0
? getMaskedCardRank(cardMasks[maskIndex])
: getReferencedRank(hand, maskIndex);
hand.push(`${rank}${suit}`);
return hand;
}, []);
}
}
}

const getMatchingSuit = (
hand: Hand,
maskIndex: number,
): [bigint, Suit] | [undefined, undefined] => {
if (maskIndex >= 0) {
const cardMask = cardMasks[maskIndex];
const rankMask = getRankMask(getMaskedCardRank(cardMask));
const matchingSuit = findKey(
suits,
(suitedCardsMask) => (rankMask & suitedCardsMask) === rankMask,
);
if (matchingSuit) {
return [cardMask, matchingSuit];
}
}

const nextRank = getReferencedRank(hand, maskIndex);
if (nextRank !== undefined && isRank(nextRank)) {
const rankMask = getRankMask(nextRank);
const matchingSuit = findKey(
suits,
(suitedCardsMask) => (rankMask & suitedCardsMask) === rankMask,
);
if (matchingSuit) {
return [RANK_BITS_MAP[nextRank], matchingSuit];
}
}

return [undefined, undefined];
};

return maskIndices.reduce((hand: Hand, maskIndex) => {
const [cardMask, matchingSuit] = getMatchingSuit(hand, maskIndex);
if (matchingSuit) {
suits[matchingSuit] -= getRankMask(getMaskedCardRank(cardMask));
hand.push(`${getMaskedCardRank(cardMask)}${matchingSuit}`);
}
return hand;
}, []);
};

export const getHand = (handMask: bigint, handValueMask: bigint, strength: HandStrength): Hand => {
const cardMasks = [
(handValueMask >> CARD_1_BIT_SHIFT) & CARD_MASK,
(handValueMask >> CARD_2_BIT_SHIFT) & CARD_MASK,
(handValueMask >> CARD_3_BIT_SHIFT) & CARD_MASK,
(handValueMask >> CARD_4_BIT_SHIFT) & CARD_MASK,
(handValueMask >> CARD_5_BIT_SHIFT) & CARD_MASK,
];

switch (strength) {
case HandStrength.HighCard:
return constructHand(handMask, cardMasks, [0, 1, 2, 3, 4]);
case HandStrength.OnePair:
return constructHand(handMask, cardMasks, [0, 0, 1, 2, 3]);
case HandStrength.TwoPair:
return constructHand(handMask, cardMasks, [0, 0, 1, 1, 2]);
case HandStrength.ThreeOfAKind:
return constructHand(handMask, cardMasks, [0, 0, 0, 1, 2]);
case HandStrength.Straight:
return constructHand(handMask, cardMasks, [0, -1, -1, -1, -1]);
case HandStrength.Flush:
return constructHand(handMask, cardMasks, [0, 1, 2, 3, 4], true);
case HandStrength.FullHouse:
return constructHand(handMask, cardMasks, [0, 0, 0, 1, 1]);
case HandStrength.FourOfAKind:
return constructHand(handMask, cardMasks, [0, 0, 0, 0, 1]);
case HandStrength.StraightFlush:
return constructHand(handMask, cardMasks, [0, -1, -1, -1, -1], true);
case HandStrength.RoyalFlush:
return constructHand(handMask, cardMasks, [0, -1, -1, -1, -1], true);
default:
return assertNever(strength);
}
};

export const getStrength = (handMask: bigint): HandStrength => {
const baseStrength: HandStrength = Number(handMask >> HAND_MASK_BIT_SHIFT);
const highCardRank = getMaskedCardRank((handMask >> CARD_1_BIT_SHIFT) & CARD_MASK);

const strength =
baseStrength === HandStrength.StraightFlush && highCardRank === 'A'
? HandStrength.RoyalFlush
: baseStrength;

return strength;
};

export const evaluateHandMask = (handMask: bigint): EvaluatedHand => {
const value = getHandValueMask(handMask);
const strength = getStrength(value);
const hand = getHand(handMask, value, strength);

return { strength, hand, value };
};
13 changes: 13 additions & 0 deletions src/utils/findKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type Entries<T> = {
[K in keyof T]-?: [K, T[K]];
}[keyof T][];

const getEntries = <T extends object>(obj: T) => Object.entries(obj) as Entries<T>;

export const findKey = <K extends PropertyKey, V>(
obj: Record<K, V>,
predicate: (v: V) => boolean,
): K | undefined => {
const [key] = getEntries(obj).find(([_key, value]) => predicate(value)) ?? [];
return key;
};
17 changes: 10 additions & 7 deletions src/utils/getHandMask.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { Card, Hand, getRank, getSuit } from '@poker-apprentice/types';
import { Card, Hand, Rank, Suit, getRank, getSuit } from '@poker-apprentice/types';
import { RANK_BITS_MAP, SUIT_BITS_MAP } from '../constants/bitmasks';

const getCardValue = (card: Card): bigint => {
const rank = RANK_BITS_MAP[getRank(card)];
const suit = SUIT_BITS_MAP[getSuit(card)];
return rank + suit * 13n;
export const getRankMask = (rank: Rank): bigint => 1n << RANK_BITS_MAP[rank];

const getSuitOffset = (suit: Suit): bigint => 13n * SUIT_BITS_MAP[suit];

const getCardMask = (card: Card): bigint => {
const rank = getRankMask(getRank(card));
const suit = getSuitOffset(getSuit(card));
return rank << suit;
};

export const getHandMask = (hand: Hand): bigint => {
let handMask = 0n;

for (const card of hand) {
const cardValue = getCardValue(card);
handMask |= 1n << cardValue;
handMask |= getCardMask(card);
}

return handMask;
Expand Down

0 comments on commit 2caf2dd

Please sign in to comment.