From 2caf2ddaf25ded89514b6b0f0492ab0bf02f69d3 Mon Sep 17 00:00:00 2001 From: Matt Huggins Date: Sun, 5 May 2024 08:56:01 -0500 Subject: [PATCH] determine hand from hand mask & value mask 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 --- src/__tests__/evaluate.test.ts | 14 +-- src/evaluate.ts | 118 +------------------------ src/utils/evaluateHandMask.ts | 157 +++++++++++++++++++++++++++++++++ src/utils/findKey.ts | 13 +++ src/utils/getHandMask.ts | 17 ++-- 5 files changed, 190 insertions(+), 129 deletions(-) create mode 100644 src/utils/evaluateHandMask.ts create mode 100644 src/utils/findKey.ts diff --git a/src/__tests__/evaluate.test.ts b/src/__tests__/evaluate.test.ts index 4168ae6..fea0595 100644 --- a/src/__tests__/evaluate.test.ts +++ b/src/__tests__/evaluate.test.ts @@ -29,7 +29,7 @@ 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, }); }); @@ -37,7 +37,7 @@ describe('evaluate', () => { 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, }); }); @@ -45,7 +45,7 @@ describe('evaluate', () => { 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, }); }); @@ -60,7 +60,7 @@ describe('evaluate', () => { }), ).toEqual({ strength: HandStrength.FullHouse, - hand: ['Kc', 'Kd', 'Kh', '5d', '5c'], + hand: ['Kc', 'Kd', 'Kh', '5c', '5d'], value: 101396480n, }); }); @@ -92,7 +92,7 @@ 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, }); }); @@ -100,7 +100,7 @@ describe('evaluate', () => { 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, }); }); @@ -171,7 +171,7 @@ describe('evaluate', () => { }), ).toEqual({ strength: HandStrength.OnePair, - hand: ['As', 'Ad'], + hand: ['Ad', 'As'], value: 17563648n, }); }); diff --git a/src/evaluate.ts b/src/evaluate.ts index adb5f22..1dd4dc5 100644 --- a/src/evaluate.ts +++ b/src/evaluate.ts @@ -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[]; @@ -97,103 +81,7 @@ const getAllHandCombinations = ({ return allHandCombinations.filter((cards) => cards.length === longestCombination); }; -const take = (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, diff --git a/src/utils/evaluateHandMask.ts b/src/utils/evaluateHandMask.ts new file mode 100644 index 0000000..110aa17 --- /dev/null +++ b/src/utils/evaluateHandMask.ts @@ -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 }; +}; diff --git a/src/utils/findKey.ts b/src/utils/findKey.ts new file mode 100644 index 0000000..bd27858 --- /dev/null +++ b/src/utils/findKey.ts @@ -0,0 +1,13 @@ +type Entries = { + [K in keyof T]-?: [K, T[K]]; +}[keyof T][]; + +const getEntries = (obj: T) => Object.entries(obj) as Entries; + +export const findKey = ( + obj: Record, + predicate: (v: V) => boolean, +): K | undefined => { + const [key] = getEntries(obj).find(([_key, value]) => predicate(value)) ?? []; + return key; +}; diff --git a/src/utils/getHandMask.ts b/src/utils/getHandMask.ts index 5667566..86f65bd 100644 --- a/src/utils/getHandMask.ts +++ b/src/utils/getHandMask.ts @@ -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;