poker-ranking
Version:
provides a rank for a poker hand, potentially including wildcards
505 lines (443 loc) • 15.4 kB
JavaScript
//
// Main routine to evaluation a poker hand
//
const suits = ['C', 'D', 'H', 'S'];
const ranks = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
module.exports = {
// Evaluate a hand
evaluateHand: function(cards, options) {
return evaluateInternal(cards, mapOptions(cards, options));
},
evaluateAndFindCards: function(cards, options) {
const playerOptions = mapOptions(cards, options);
playerOptions.getDetails = true;
return evaluateInternal(cards, playerOptions);
},
};
function evaluateInternal(cards, playerOptions) {
const hand = createHandArray(cards, playerOptions);
let matchedCards;
let match;
if (!hand) {
match = 'error';
} else {
const flush = isHandFlush(hand, playerOptions);
const straightHiCard = getStraightHighCard(hand, playerOptions);
const maxLikeCards = getMaxLikeCards(hand, playerOptions);
// OK, let's see what we got - highest hand, 5-of-a-kind (with wild cards)
if (maxLikeCards.maxLike === 5) {
match = '5ofakind';
matchedCards = maxLikeCards.cards;
} else if (flush.isFlush && (straightHiCard.hiCard === 14) && (playerOptions.dontAllow.indexOf('royalflush') < 0)) {
match = 'royalflush';
matchedCards = flush.cards;
} else if (flush.isFlush && (straightHiCard.hiCard > 0) && (playerOptions.dontAllow.indexOf('straightflush') < 0)) {
match = 'straightflush';
matchedCards = flush.cards;
} else if ((maxLikeCards.maxLike === 4) && (playerOptions.dontAllow.indexOf('4ofakind') < 0)) {
match = '4ofakind';
matchedCards = maxLikeCards.cards;
} else {
const fullHouse = getFullHouse(hand, playerOptions);
if (fullHouse.isFullHouse && (playerOptions.dontAllow.indexOf('fullhouse') < 0)) {
match = 'fullhouse';
matchedCards = fullHouse.cards;
} else if (flush.isFlush && (playerOptions.dontAllow.indexOf('flush') < 0)) {
match = 'flush';
matchedCards = flush.cards;
} else if ((straightHiCard.hiCard > 0) && (playerOptions.dontAllow.indexOf('straight') < 0)) {
match = 'straight';
matchedCards = straightHiCard.cards;
} else if ((maxLikeCards.maxLike === 3) && (playerOptions.dontAllow.indexOf('3ofakind') < 0)) {
match = '3ofakind';
matchedCards = maxLikeCards.cards;
} else {
const twoPair = getTwoPair(hand, playerOptions);
if (twoPair.isTwoPair && (playerOptions.dontAllow.indexOf('2pair') < 0)) {
match = '2pair';
matchedCards = twoPair.cards;
} else if ((maxLikeCards.maxLike === 2) && (playerOptions.dontAllow.indexOf('pair') < 0)) {
// Was a minimum pair set?
if (playerOptions.minPair) {
// What is the pair?
const pairCard = getPairCard(hand);
if (pairCard >= (ranks.indexOf(playerOptions.minPair) + 1)) {
match = 'minpair';
}
}
if (!match) {
match = 'pair';
}
matchedCards = maxLikeCards.cards;
} else {
// Nothing - matched cards remains undefined
match = 'nothing';
matchedCards = [];
}
}
}
}
if (playerOptions.getDetails) {
// If a callback is provided, return the matched cards
return {match: match, cards: matchedCards};
} else {
return match;
}
}
// Maps the options (if any) to fill in default values
function mapOptions(cards, options) {
const playerOptions = {aceCanBeLow: false,
wildCards: ['JOKER'],
cardsToEvaluate: 5,
dontAllow: []};
if (options) {
if (options.hasOwnProperty('aceCanBeLow')) {
playerOptions.aceCanBeLow = options.aceCanBeLow;
}
if (options.hasOwnProperty('cardsToEvaluate')) {
playerOptions.cardsToEvaluate = options.cardsToEvaluate;
}
if (options.hasOwnProperty('dontAllow')) {
playerOptions.dontAllow = options.dontAllow;
}
if (options.hasOwnProperty('minPair')) {
// Only keep if it's valid
if (ranks.indexOf(options.minPair.toUpperCase()) > -1) {
playerOptions.minPair = options.minPair.toUpperCase();
}
}
// Now map any wild cards - the array passed in can be a single rank (e.g. '2' or 'K')
// or can be specific cards (e.g. 'JH', 'JD' for red jacks)
// Either way, we will expand this to an array of individual cards
if (options.hasOwnProperty('wildCards')) {
let i;
let exactCard;
let value;
for (i = 0; i < options.wildCards.length; i++) {
// Ignore Joker - we already put that in
const wildCard = options.wildCards[i].toUpperCase();
if (wildCard !== 'JOKER') {
exactCard = getRankAndSuit(wildCard);
if (exactCard) {
playerOptions.wildCards.push(wildCard);
} else {
// Not an exact card, so it should be just a rank
value = getRank(wildCard);
if (value > 0) {
suits.map((suit) => playerOptions.wildCards.push(wildCard + suit));
}
}
}
}
}
}
// Oh, you can't have more than 5 (or cards.length) in the cardsToEvaluate
if (playerOptions.cardsToEvaluate > cards.length) {
playerOptions.cardsToEvaluate = cards.length;
}
if (playerOptions.cardsToEvaluate > 5) {
playerOptions.cardsToEvaluate = 5;
}
return playerOptions;
}
// Given a string (e.g. '10' or 'J'), this function returns the rank, which
// is a number from 1-14. A return value of 0 indicates an error
function getRank(rankString) {
let rank;
if (rankString === '10') {
rank = 10;
} else if (rankString.length === 1) {
// Rank is 2-9, J, Q, K, or A
rank = ranks.indexOf(rankString.toUpperCase()) + 1;
if (rank < 2) {
// Nope, bad input
rank = undefined;
}
}
return rank;
}
// Given a card string (e.g. '2s'), this function returns an object giving the
// rank and the suit, where rank is a number from 1-14 and suit from 0-3
function getRankAndSuit(card) {
const result = {rank: 0, suit: 0};
let suitString;
if (card.substring(0, 2) === '10') {
result.rank = 10;
suitString = card.substring(2, card.length);
} else if (card.length === 2) {
const rank = getRank(card.substring(0, 1));
if ((rank === undefined) || (rank < 2)) {
// Nope, bad input
return undefined;
}
result.rank = rank;
suitString = card.substring(1, 2);
} else {
// Bad input
return undefined;
}
// OK, we set the rank, now find a suit
result.suit = suits.indexOf(suitString.toUpperCase());
if (result.suit < 0) {
// Sorry, bad suit
return undefined;
}
// OK, return the card
return result;
}
// This function maps the array of player cards into data in the result object:
// 1) Suits: An array giving the number of clubs, diamonds, hearts, and spades (in that order)
// 2) Rank: An array giving the number of cards of each rank;
// if Aces can be low they are counted twice (position 0 and 14)
// 3) WildCards: The number of wild cards in the hand.
// Note wildcards do NOT go into the above arrays
function createHandArray(cards, options) {
let i;
let card;
const result = {suits: [0, 0, 0, 0],
rank: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
wildCards: 0};
for (i = 0; i < cards.length; i++) {
// First check if this string is in the list of wildcards
if (options.wildCards.indexOf(cards[i].toUpperCase()) >= 0) {
// We have a wild card!
result.wildCards++;
} else {
// Get the rank and the suit
card = getRankAndSuit(cards[i]);
if (!card) {
// Bad input
return null;
}
result.suits[card.suit]++;
result.rank[card.rank - 1]++;
if ((card.rank == 14) && options.aceCanBeLow) {
// This is an ace - it can also be low
// This is only an issue for straight, so don't mark
// more than 1 low ace (to avoid it being counted as two pair)
result.rank[0] = 1;
}
}
}
// OK, let's do it
result.cards = cards;
return result;
}
// Determines whether a hand is a flush or not
function isHandFlush(hand, options) {
const result = {};
if ((Math.max.apply(null, hand.suits) + hand.wildCards) >= options.cardsToEvaluate) {
// Return the cards that make the flush if requested
result.isFlush = true;
if (options.getDetails) {
// Return value needs to be the matching cards
const cards = [];
// We would suggest starting with wild cards
getWildCards(cards, hand, options);
getLikeSuit(cards, hand, options, Math.max.apply(null, hand.suits));
result.cards = cards;
}
} else {
// Not a flush
result.isFlush = false;
}
return result;
}
// Determines whether a hand is a straight or not, returning
// the high card in that straight. If the hand is not a straight,
// then this function returns 0
function getStraightHighCard(hand, options) {
let hiCard = 0;
let i;
let curRun = 0;
let wildRun = hand.wildCards;
const result = {};
for (i = 0; i < hand.rank.length; i++) {
if (hand.rank[i]) {
// OK, add to the current run
curRun++;
} else {
// If there are wild cards, they can be used here
if ((curRun > 0) && (wildRun > 0)) {
wildRun--;
curRun++;
} else {
// The current run is over
if (curRun >= options.cardsToEvaluate) {
// And it's a straight!
hiCard = i;
}
curRun = 0;
wildRun = hand.wildCards;
}
}
}
// It's possible that we have an Ace-high straight, or that
// wild cards could be used to complete an Ace-high straight
if ((curRun + wildRun) >= options.cardsToEvaluate) {
// Ace-high striaght!
hiCard = hand.rank.length;
}
result.hiCard = hiCard;
if (hiCard && options.getDetails) {
// Return the details of the straight - start with wild
const cards = [];
const used = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
getWildCards(cards, hand, options);
// Now, let's look thru each non-wild card and see if it's
// in scope for the definition of this straight
hand.cards.map((card) => {
let rank = getRankAndSuit(card).rank - 1;
// Is this an ace-low straight?
if ((rank === 13) && (hiCard === options.cardsToEvaluate)) {
rank = 0;
}
if ((cards.length < options.cardsToEvaluate)
&& (options.wildCards.indexOf(card.toUpperCase()) < 0)
&& (rank <= hiCard)
&& (rank >= hiCard - options.cardsToEvaluate)
&& (!used[rank])) {
cards.push(card);
// Mark this as used so we don't include a duplicate
used[rank] = 1;
}
});
result.cards = cards;
}
return result;
}
// Returns the maximum number of like cards (pair, three of a kind, etc)
function getMaxLikeCards(hand, options) {
let maxLike = Math.max.apply(null, hand.rank) + hand.wildCards;
const result = {};
if (maxLike > hand.cardsToEvaluate) {
maxLike = hand.cardsToEvaluate;
}
if (options.getDetails) {
// Return what the matched cards are as well
const cards = [];
// We would suggest starting with wild cards
getWildCards(cards, hand, options);
getLikeCards(cards, hand, options, Math.max.apply(null, hand.rank));
result.cards = cards;
}
result.maxLike = maxLike;
return result;
}
function getFullHouse(hand, options) {
const result = {isFullHouse: false};
// You need 5 cards to evaluate to make a full house
if (options.cardsToEvaluate >= 5) {
if (hand.rank.indexOf(3) >= 0) {
// OK, we have three of a kind (natural) - now look for a pair
// No need to check for wild cards as they would have 4-of-a-kind instead
if (hand.rank.indexOf(2) >= 0) {
// Natural full house! Get the specific cards if necessary
result.isFullHouse = true;
if (options.getDetails) {
// Which rank is 3 and which is 2?
const cards = [];
getLikeCards(cards, hand, options, 3);
getLikeCards(cards, hand, options, 2);
result.cards = cards;
}
}
} else if (hand.wildCards > 0) {
// OK, they have wild cards; they can't have three wild cards (else it's 4-of-a-kind)
// and they can't have two - that would require a natural pair which would be 4-of-a-kind
// So we just need to check for two pairs (natural) - and that's just what getTwoPair does
const twoPair = getTwoPair(hand, options);
if (twoPair.isTwoPair) {
result.isFullHouse = true;
if (options.getDetails) {
result.cards = twoPair.cards;
getWildCards(result.cards, hand, options);
}
}
}
}
return result;
}
function getTwoPair(hand, options) {
const result = {};
// No need to check for wild cards as
// any wild cards would make this a better hand than just two pair
result.isTwoPair = (hand.rank.reduce((sum, value) => {
return (value === 2) ? (sum + 1) : sum;
}, 0) >= 2);
if (result.isTwoPair && options.getDetails) {
// Now we have to find the cards that are two pair
const handCopy = JSON.parse(JSON.stringify(hand));
const cards = [];
// First pair
getLikeCards(cards, handCopy, options, 2);
// Now 0 these out from handCopy
handCopy.rank[getRankAndSuit(cards[0]).rank - 1] = 0;
getLikeCards(cards, handCopy, options, 2);
result.cards = cards;
}
return result;
}
// Function assumes there is a single pair
function getPairCard(hand) {
const card = hand.rank.indexOf(2, 1);
if (card > -1) {
return (card + 1);
}
// OK, so there's a wild card - return the highest card in array
let i;
for (i = hand.rank.length - 1; i--; i > 0) {
if (hand.rank[i]) {
return i;
}
}
// This shouldn't happen
return undefined;
}
function getLikeCards(cards, hand, options, likeRank) {
let matchRank;
let i;
for (i = 0; i < hand.rank.length; i++) {
if (hand.rank[i] === likeRank) {
// This is the high suit
matchRank = i;
}
}
// Now find cards that match this rank (and aren't wild)
hand.cards.map((card) => {
if ((cards.length < options.cardsToEvaluate)
&& (options.wildCards.indexOf(card.toUpperCase()) < 0)
&& ((getRankAndSuit(card).rank - 1) === matchRank)) {
cards.push(card);
}
});
}
function getLikeSuit(cards, hand, options, likeSuit) {
// Now go thru and see which cards from the suit match
let matchSuit;
let i;
for (i = 0; i < hand.suits.length; i++) {
if (hand.suits[i] === likeSuit) {
// This is the high suit
matchSuit = i;
}
}
// Now find cards that match this suit (and aren't wild)
hand.cards.map((card) => {
if ((cards.length < options.cardsToEvaluate)
&& (options.wildCards.indexOf(card.toUpperCase()) < 0)
&& (getRankAndSuit(card).suit === matchSuit)) {
cards.push(card);
}
});
}
function getWildCards(cards, hand, options) {
// Return the details of the straight - start with wild
hand.cards.map((card) => {
if (options.wildCards.indexOf(card.toUpperCase()) >= 0) {
if (cards.length < options.cardsToEvaluate) {
cards.push(card);
}
}
});
}