// Poker Hand Evaluator by Andrey Kurdyumov ©2013 // v1.0.0 /* tslint:disable:no-bitwise */ // tslint:disable-next-line:no-namespace namespace HoldemHand { export interface HandRepresentation { Cards: number[]; Suits: number[]; } export enum HandParseResultStatus { Ok, InvalidHand, CardsMissing, SuitsMissing, AllCardsShouldHaveOneSuit, InsufficientCardsAndDuplicates, InsufficientCards, DuplicatesDetected, } export interface HandParseResult { Status: HandParseResultStatus; Hand?: HandRepresentation; } export interface CardRank { HandType: number; // Hand type. First part of universal card strength. Score: number; // Score of the hand within same hand type. Second part of universal card strength. WinnerCardsSet: number[]; // Indexes of the cards within original hand which form the best card combination. } /// Human readable names of the hand types. export let handTypeNames = ["4 of a Kind", "Straight Flush", "Straight", "Flush", "High Card", "1 Pair", "2 Pair", "Royal Flush", "3 of a Kind", "Full House", "-Invalid-"]; /// Strength of each hand type. /// /// Due to nature of the algorithm, the hand types not /// have order corresponding to the strength or cards combinations. /// export let handTypeRanks = [8, 9, 5, 6, 1, 2, 3, 10, 4, 7, 0]; /** * Gets type of hand for given hand representation. * @param hand 5 cards hand for which current combination should be calculated. * @return Hand type. * @description To obtain real strength of hand type, use hand * type returned from this function as index to the handTypeRanks array. */ export function getHandType(hand: HandRepresentation) { let rankCountBitMask = 0; // index of the card. let o = 0; let rankBitMask; for (let i = -1; i < 5; i++, o = Math.pow(2, hand.Cards[i] * 4)) { rankCountBitMask += o * ((rankCountBitMask / o & 15) + 1); } // tslint:disable-next-line:no-conditional-assignment if ((rankCountBitMask %= 15) !== 5) { return rankCountBitMask - 1; } rankBitMask = 1 << hand.Cards[0] | 1 << hand.Cards[1] | 1 << hand.Cards[2] | 1 << hand.Cards[3] | 1 << hand.Cards[4]; rankCountBitMask -= ((rankBitMask / (rankBitMask & -rankBitMask) === 31) || (rankBitMask === 0x403c) ? 3 : 1); // tslint:disable-next-line:max-line-length const isSameSuit = (hand.Suits[0] === (hand.Suits[0] | hand.Suits[1] | hand.Suits[2] | hand.Suits[3] | hand.Suits[4])) as any; return rankCountBitMask - isSameSuit * ((rankBitMask === 0x7c00) ? -5 : 1); } export function cardValue(v: number) { if (v === 14) { return "A"; } if (v === 13) { return "K"; } if (v === 12) { return "Q"; } if (v === 11) { return "J"; } return v.toString(); } export function getHandTypeEx(hand: HandRepresentation) { // tslint:disable-next-line:max-line-length const isSameSuit = hand.Suits[0] === (hand.Suits[0] | hand.Suits[1] | hand.Suits[2] | hand.Suits[3] | hand.Suits[4]); let sortedCards = hand.Cards.slice(0); sortedCards = sortedCards.sort(function (left, right) { if (left < right) { return 1; } if (left > right) { return -1; } return 0; }); let isStraight = sortedCards[0] === sortedCards[1] + 1 && sortedCards[1] === sortedCards[2] + 1 && sortedCards[2] === sortedCards[3] + 1 && sortedCards[3] === sortedCards[4] + 1; if (!isStraight) { isStraight = sortedCards[3] === sortedCards[4] + 1 && sortedCards[1] === sortedCards[2] + 1 && sortedCards[2] === sortedCards[3] + 1 && sortedCards[4] === 2 && sortedCards[0] === 14; } if (isSameSuit && isStraight) { return { Type: 1, Cards: [sortedCards[0]], }; } const isCare = (sortedCards[0] === sortedCards[1] && sortedCards[1] === sortedCards[2] && sortedCards[2] === sortedCards[3]) || (sortedCards[1] === sortedCards[2] && sortedCards[2] === sortedCards[3] && sortedCards[3] === sortedCards[4]); if (isCare) { if (sortedCards[0] === sortedCards[1]) { return { Type: 0, Cards: [sortedCards[0], sortedCards[4]], }; } return { Type: 0, Cards: [sortedCards[1], sortedCards[0]], }; } const isFullHouse = (sortedCards[0] === sortedCards[1] && sortedCards[1] === sortedCards[2] && sortedCards[3] === sortedCards[4]) || (sortedCards[0] === sortedCards[1] && sortedCards[2] === sortedCards[3] && sortedCards[3] === sortedCards[4]); if (isFullHouse) { return { Type: 9, Cards: [sortedCards[1], sortedCards[3]], }; } if (isSameSuit) { return { Type: 3, Cards: sortedCards, }; } if (isStraight) { return { Type: 2, Cards: [Math.max(sortedCards[0], sortedCards[4])], }; } // is triple if (sortedCards[0] === sortedCards[1] && sortedCards[1] === sortedCards[2]) { return { Type: 8, Cards: [sortedCards[0], sortedCards[3], sortedCards[4]], }; } if (sortedCards[1] === sortedCards[2] && sortedCards[2] === sortedCards[3]) { return { Type: 8, Cards: [sortedCards[1], sortedCards[0], sortedCards[4]], }; } if (sortedCards[2] === sortedCards[3] && sortedCards[3] === sortedCards[4]) { return { Type: 8, Cards: [sortedCards[2], sortedCards[0], sortedCards[1]], }; } // is 2 pair if (sortedCards[0] === sortedCards[1] && sortedCards[2] === sortedCards[3]) { return { Type: 6, Cards: [sortedCards[0], sortedCards[2], sortedCards[4]], }; } if (sortedCards[0] === sortedCards[1] && sortedCards[3] === sortedCards[4]) { return { Type: 6, Cards: [sortedCards[0], sortedCards[3], sortedCards[2]], }; } if (sortedCards[1] === sortedCards[2] && sortedCards[3] === sortedCards[4]) { return { Type: 6, Cards: [sortedCards[1], sortedCards[3], sortedCards[0]], }; } // is pair if (sortedCards[0] === sortedCards[1]) { return { Type: 5, Cards: [sortedCards[0], sortedCards[2], sortedCards[3], sortedCards[4]], }; } if (sortedCards[1] === sortedCards[2]) { return { Type: 5, Cards: [sortedCards[1], sortedCards[0], sortedCards[3], sortedCards[4]], }; } if (sortedCards[2] === sortedCards[3]) { return { Type: 5, Cards: [sortedCards[2], sortedCards[0], sortedCards[1], sortedCards[4]], }; } if (sortedCards[3] === sortedCards[4]) { return { Type: 5, Cards: [sortedCards[3], sortedCards[0], sortedCards[1], sortedCards[2]], }; } return { Type: 4, Cards: sortedCards, }; } export function getCombinations(k: number, n: number) { /// /// /// Generates all possible unordered permutations /// of k element in the set with n elements. /// /// const result: number[][] = []; const combination: number[] = []; function next_comb(comb: number[], k1: number, n1: number) { let i: number; if (comb.length === 0) { for (i = 0; i < k1; ++i) { comb[i] = i; } return true; } i = k1 - 1; ++comb[i]; while ((i > 0) && (comb[i] >= n1 - k1 + 1 + i)) { --i; ++comb[i]; } // No more combinations can be generated if (comb[0] > n1 - k1) { return false; } for (i = i + 1; i < k1; ++i) { comb[i] = comb[i - 1] + 1; } return true; } while (next_comb(combination, k, n)) { const nextPermutation = combination.slice(null); result.push(nextPermutation); } return result; } export function decodeScore(score: number) { const result = []; result.push((score & 15)); score = score >> 4; result.push((score & 15)); score = score >> 4; result.push((score & 15)); score = score >> 4; result.push((score & 15)); return result; } export function getPokerScore(cards: number[]) { const tempCards = cards.slice(0); const cardsCount = {}; for (let i = 0; i < 5; i++) { const tempCardValue = tempCards[i]; cardsCount[tempCardValue] = (cardsCount[tempCardValue] >= 1) ? cardsCount[tempCardValue] + 1 : 1; } tempCards.sort(function (left, right) { if (cardsCount[left] < cardsCount[right]) { return 1; } if (cardsCount[left] > cardsCount[right]) { return -1; } return right - left; }); return tempCards[0] << 16 | tempCards[1] << 12 | tempCards[2] << 8 | tempCards[3] << 4 | tempCards[4]; } export function parseHand(str: string): HandParseResult { /// Convert string to internal representation. /// String representation of the hand which should be converted. /// Parsing result of internal conversion. if (str.match(/((?:\s*)(10|[2-9]|[J|Q|K|A])[♠|♣|♥|♦](?:\s*)){1,7}/g) === null) { return { Status: HandParseResultStatus.InvalidHand }; } const cardStr = str.replace(/A/g, "14").replace(/K/g, "13").replace(/Q/g, "12") .replace(/J/g, "11").replace(/♠|♣|♥|♦/g, ","); const cards = cardStr.replace(/\s/g, "").slice(0, -1).split(",") as any[]; const suits = str.match(/♠|♣|♥|♦/g) as any[]; if (cards === null) { return { Status: HandParseResultStatus.CardsMissing }; } if (suits === null) { return { Status: HandParseResultStatus.SuitsMissing }; } if (cards.length !== suits.length) { return { Status: HandParseResultStatus.AllCardsShouldHaveOneSuit }; } const o = {}; let keyCount = 0; for (let i = 0; i < cards.length; i++) { const e = cards[i] + suits[i]; o[e] = 1; } for (const j in o) { if (o.hasOwnProperty(j)) { keyCount++; } } const insufficientCards = cards.length < 5; const duplicateCards = cards.length !== keyCount; let status: HandParseResultStatus; if (insufficientCards && duplicateCards) { status = HandParseResultStatus.InsufficientCardsAndDuplicates; } else if (insufficientCards) { status = HandParseResultStatus.InsufficientCards; } else if (duplicateCards) { status = HandParseResultStatus.DuplicatesDetected; } else { status = HandParseResultStatus.Ok; } for (let i = 0; i < cards.length; i++) { cards[i] -= 0; // Conversion to the numbers. } for (let i = 0; i < suits.length; i++) { // Convert the suit characters to the numbers. // Since unicode symbols starts with 0x2660 (9824) Unicode symbols suits[i] = Math.pow(2, (suits[i].charCodeAt(0) - 9824)); } return { Status: status, Hand: { Cards: cards, Suits: suits }, }; } /** * Get universal hand strength * @param hand Hand representation for which strength should be converted. * @return Rank of the card across all possible hands. * @description This function could accept hands from 5 to 7 cards. */ export function getCardRank(hand: HandRepresentation): CardRank { const totalCardsCount = hand.Cards.length; const permutations = getCombinations(5, totalCardsCount); let maxRank = 0; let winIndex = 10; let winningScore = -1; let wci: number[]; // Generate permuted version of the original array. const applyPermutation5 = function (source: number[], permutation: number[]) { return [ source[permutation[0]], source[permutation[1]], source[permutation[2]], source[permutation[3]], source[permutation[4]], ]; }; for (let i = 0; i < permutations.length; i++) { const currentPermutation = permutations[i]; const cs = applyPermutation5(hand.Cards, currentPermutation); const ss = applyPermutation5(hand.Suits, currentPermutation); const index = getHandType({ Cards: cs, Suits: ss }); if (handTypeRanks[index] > maxRank) { maxRank = handTypeRanks[index]; winIndex = index; wci = currentPermutation.slice(0); winningScore = getPokerScore(cs); } else if (handTypeRanks[index] === maxRank) { // If by chance we have a tie, find the best one const score1 = getPokerScore(cs); if (score1 > winningScore) { wci = currentPermutation.slice(0); } } } return { HandType: winIndex, Score: winningScore, WinnerCardsSet: wci, }; } }