holdemhandranker
Version:
Ranks and evaluates Texas Holdem hands
748 lines (628 loc) • 20.4 kB
JavaScript
var _ = require('lodash');
// As a reminder, not used in code
var _CARDS = [
['Ah', '2h', '3h', '4h', '5h', '6h', '7h', '8h', '9h', 'Th', 'Jh', 'Qh', 'Kh'],
['As', '2s', '3s', '4s', '5s', '6s', '7s', '8s', '9s', 'Ts', 'Js', 'Qs', 'Ks'],
['Ac', '2c', '3c', '4c', '5c', '6c', '7c', '8c', '9c', 'Tc', 'Jc', 'Qc', 'Kc'],
['Ad', '2d', '3d', '4d', '5d', '6d', '7d', '8d', '9d', 'Td', 'Jd', 'Qd', 'Kd']
];
/** HAND TYPES */
var ROYAL_FLUSH = 1;
var STRAIGHT_FLUSH = 2;
var QUADS = 3;
var FULL_HOUSE = 4;
var FLUSH = 5;
var STRAIGHT = 6;
var TRIPS = 7;
var TWO_PAIRS = 8;
var PAIR = 9;
var HIGH_CARD = 10;
/** Creates all five-card combinations out of seven cards */
var createAllFiveCombos = function(cards) {
var combos = [];
// Comment after shows which two are missing from the selection
combos.push([cards[0], cards[1], cards[2], cards[3], cards[4]]); // 5,6
combos.push([cards[0], cards[1], cards[2], cards[3], cards[5]]); // 4,6
combos.push([cards[0], cards[1], cards[2], cards[5], cards[4]]); // 3,6
combos.push([cards[0], cards[1], cards[5], cards[3], cards[4]]); // 2,6
combos.push([cards[0], cards[5], cards[2], cards[3], cards[4]]); // 1,6
combos.push([cards[5], cards[1], cards[2], cards[3], cards[4]]); // 0,6
combos.push([cards[0], cards[1], cards[2], cards[3], cards[6]]); // 4,5
combos.push([cards[0], cards[1], cards[2], cards[4], cards[6]]); // 3,5
combos.push([cards[0], cards[1], cards[3], cards[6], cards[4]]); // 2,5
combos.push([cards[0], cards[2], cards[6], cards[3], cards[4]]); // 1,5
combos.push([cards[1], cards[6], cards[2], cards[3], cards[4]]); // 0,5
combos.push([cards[0], cards[1], cards[2], cards[5], cards[6]]); // 3,4
combos.push([cards[0], cards[1], cards[3], cards[6], cards[5]]); // 2,4
combos.push([cards[0], cards[6], cards[2], cards[5], cards[3]]); // 1,4
combos.push([cards[2], cards[1], cards[5], cards[3], cards[6]]); // 0,4
combos.push([cards[0], cards[1], cards[6], cards[4], cards[5]]); // 2,3
combos.push([cards[0], cards[6], cards[2], cards[5], cards[4]]); // 1,3
combos.push([cards[2], cards[1], cards[5], cards[6], cards[4]]); // 0,3
combos.push([cards[0], cards[6], cards[3], cards[5], cards[4]]); // 1,2
combos.push([cards[6], cards[1], cards[5], cards[3], cards[4]]); // 0,2
combos.push([cards[6], cards[2], cards[5], cards[3], cards[4]]); // 0,1
return combos;
}
/**
* Returns sorted array of kicker values
* I.e ['Ah', 'Kh', 'Qh', '2h', 'Jh'] -> [14, 13, 12, 11, 2]
*/
var getKickersOfHand = function(combo) {
var numerics = _.map(combo, function(card) {
var char = card.charAt(0);
if (char === 'A') return 14;
if (char === 'K') return 13;
if (char === 'Q') return 12;
if (char === 'J') return 11;
if (char === 'T') return 10;
// Its a string of int already
return parseInt(char);
});
return numerics.sort(function(a,b) {
return b-a;
})
}
/** Evaluates a five-card combination */
var evaluateCombo = function(combo) {
if (handTypeCheck.isRoyalFlush(combo)) {
return {
handType: 'royalFlush',
handRank: ROYAL_FLUSH,
kickers: [14,13,12,11,10] // Can be hard-coded, royal flush is T->A
}
}
if (handTypeCheck.isStraightFlush(combo)) {
return {
handType: 'straightFlush',
handRank: STRAIGHT_FLUSH,
kickers: getKickersOfHand(combo)
}
}
if (handTypeCheck.isFourOfAKind(combo)) {
return {
handType: 'fourOfAKind',
handRank: QUADS,
kickers: getKickersOfHand(combo)
}
}
if (handTypeCheck.isFullHouse(combo)) {
return {
handType: 'fullHouse',
handRank: FULL_HOUSE,
kickers: getKickersOfHand(combo)
}
}
if (handTypeCheck.isFlush(combo)) {
return {
handType: 'flush',
handRank: FLUSH,
kickers: getKickersOfHand(combo)
}
}
// We test separately for A->2->3->4->5 straight so we can convert ace to 1
if (handTypeCheck.isLowestStraight(combo)) {
return {
handType: 'straight',
handRank: STRAIGHT,
kickers: [5,4,3,2,1] // Kickers hard-coded for this special case with ace = 1
}
}
if (handTypeCheck.isStraight(combo)) {
return {
handType: 'straight',
handRank: STRAIGHT,
kickers: getKickersOfHand(combo)
}
}
if (handTypeCheck.isThreeOfAKind(combo)) {
return {
handType: 'threeOfAKind',
handRank: TRIPS,
kickers: getKickersOfHand(combo)
}
}
if (handTypeCheck.isTwoPairs(combo)) {
return {
handType: 'twoPairs',
handRank: TWO_PAIRS,
kickers: getKickersOfHand(combo)
}
}
if (handTypeCheck.isPair(combo)) {
return {
handType: 'pair',
handRank: PAIR,
kickers: getKickersOfHand(combo)
}
}
// Default
return {
handType: 'highCard',
handRank: HIGH_CARD,
kickers: getKickersOfHand(combo)
}
}
/**
* Contains all the methods for checking whether a five-card combination
* is of a particular hand evaluation
*
* NOTE: These methods should be called in descending-value order!
* For example, combination ['2h', '3h', '4h', '5h', '6h'] returns true
* for flush, straight, and straight flush. You should call methods in such order
* that straight flush (as most valuable) is checked first, then flush, then straight.
*/
var handTypeCheck = {
isRoyalFlush: function(combo) {
// Royal flush is simply flush and ace-high straight at the same time
return this.isFlush(combo) && this.isHighestStraight(combo);
},
isStraightFlush: function(combo) {
return this.isFlush(combo) && this.isStraight(combo);
},
isFourOfAKind: function(combo) {
var kickers = getKickersOfHand(combo);
var firstFour = _.initial(kickers);
var lastFour = _.tail(kickers);
return allTheSameInArray(firstFour) || allTheSameInArray(lastFour);
},
isFullHouse: function(combo) {
var kickers = getKickersOfHand(combo);
// Either full house composition is 2-3, or 3-2
if (allTheSameInArray(_.take(kickers, 2)) && allTheSameInArray(_.takeRight(kickers, 3))) {
return true;
}
if (allTheSameInArray(_.take(kickers, 3)) && allTheSameInArray(_.takeRight(kickers, 2))) {
return true;
}
return false;
},
isFlush: function(combo) {
var suits = _.map(combo, function(card) {
return card.charAt(1);
})
return allTheSameInArray(suits);
},
// Special case for handling ace-high straight
isHighestStraight: function(combo) {
var kickers = getKickersOfHand(combo);
return (kickers[0] === 14
&& kickers[1] === 13
&& kickers[2] === 12
&& kickers[3] === 11
&& kickers[4] === 10)
},
// Special case for handling ace-low straight
isLowestStraight: function(combo) {
var kickers = getKickersOfHand(combo);
return (kickers[0] === 14
&& kickers[1] === 5
&& kickers[2] === 4
&& kickers[3] === 3
&& kickers[4] === 2)
},
isStraight: function(combo) {
var kickers = getKickersOfHand(combo);
////console.log("isStraight")
// Check if has ace, we need to create two versions
if (hasAceInKickers(kickers)) {
// Create version where 14 is 1
var sndVersion = _.map(kickers, function(kicker) {
if (kicker === 14) return 1;
return kicker;
}).sort(function(a,b) {
return b-a;
})
return kickersInDecreasingOrder(kickers) || kickersInDecreasingOrder(sndVersion);
}
return kickersInDecreasingOrder(kickers);
},
isThreeOfAKind: function(combo) {
var kickers = getKickersOfHand(combo);
//console.log("Kickers");
//console.log(kickers)
// Possible trips (after sorted): xxx00, 0xxx0, 00xxx
var first = _.take(kickers, 3);
var middle = _.tail(_.take(kickers, 4));
var last = _.takeRight(kickers, 3);
return (allTheSameInArray(first) || allTheSameInArray(middle) || allTheSameInArray(last));
},
isTwoPairs: function(combo) {
var kickers = getKickersOfHand(combo);
// Possible two pairs (after sorted): xxyy0, xx0yy, 0xxyy
//console.log(kickers);
// xxyy0
var firstPair = _.take(kickers,2);
var sndPair = _.takeRight(_.initial(kickers), 2);
if (allTheSameInArray(firstPair) && allTheSameInArray(sndPair)) return true;
// xx0yy
sndPair = _.takeRight(kickers, 2);
if (allTheSameInArray(firstPair) && allTheSameInArray(sndPair)) return true;
// 0xxyy
firstPair = _.take(_.tail(kickers), 2);
if (allTheSameInArray(firstPair) && allTheSameInArray(sndPair)) return true;
return false;
},
isPair: function(combo) {
// Possible pairs: xx000, 0xx00, 00xx0, 000xx
var kickers = getKickersOfHand(combo);
if (allTheSameInArray(_.take(kickers, 2))) return true;
if (allTheSameInArray(_.take(_.tail(kickers), 2))) return true;
if (allTheSameInArray(_.takeRight(_.initial(kickers), 2))) return true;
if (allTheSameInArray(_.takeRight(kickers, 2))) return true;
return false;
},
}
// Helper
function hasAceInKickers(kickers) {
return !!_.find(kickers, function(kicker) {
return kicker === 14;
})
}
// Helper
function allTheSameInArray(arr) {
for(var i = 1; i < arr.length; i++)
{
if(arr[i] !== arr[0])
return false;
}
return true;
}
// Helper
function kickersInDecreasingOrder(kickers) {
////console.log(kickers);
var first = kickers[0];
var next = first - 1;
var nofail = true;
_.forEach(_.tail(kickers), function(kicker) {
if (kicker !== next) nofail = false;
next--;
})
return nofail;
}
// Helper
function findOutBestByKickers(evalInfos) {
// Multiply highest by 10^8, next 10^6, next 10^4, next 10^2, next 10^0 and sum up.
var bestEvalInfo =_.chain(evalInfos)
.map(function(evalInfo) {
var kickers = evalInfo.evaluation.kickers;
return {
kickersWorth:
// Kickers are already in decreasing order
kickers[0] * Math.pow(10, 8) +
kickers[1] * Math.pow(10, 6) +
kickers[2] * Math.pow(10, 4) +
kickers[3] * Math.pow(10, 2) +
kickers[4] * Math.pow(10, 0)
,
evalInfo: evalInfo
}
})
.sortBy(function(summedEvaluation) {
return (-1) * summedEvaluation.kickersWorth;
})
.head()
.get('evalInfo')
.value();
//console.log("Best eval info out of many");
//console.log(bestEvalInfo);
return bestEvalInfo
}
// Helper
function findOutBestByKickersDrawPossible(evalObjs) {
// Multiply highest by 10^8, next 10^6, next 10^4, next 10^2, next 10^0 and sum up.
var bestEvalInfos =_.chain(evalObjs)
.map(function(evalObj) {
var kickers = evalObj.evalInfo.kickers;
return {
id: evalObj.id,
kickersWorth:
// Kickers are already in decreasing order
kickers[0] * Math.pow(10, 8) +
kickers[1] * Math.pow(10, 6) +
kickers[2] * Math.pow(10, 4) +
kickers[3] * Math.pow(10, 2) +
kickers[4] * Math.pow(10, 0)
,
evalInfo: evalObj.evalInfo
}
})
.sortBy(function(summedEvaluation) {
return (-1) * summedEvaluation.kickersWorth;
})
.value();
// Check if multiple hands are drawing the best kickers
var checkedAgainst = bestEvalInfos[0];
return _.filter(bestEvalInfos, function(evalInfo) {
return evalInfo.kickersWorth === checkedAgainst.kickersWorth;
})
}
// Helper
function rankCards(cards) {
return _.chain(cards)
.map(function(card) {
var cardVal = card.charAt(0);
if (cardVal === 'A') cardVal = 14;
else if (cardVal === 'K') cardVal = 13;
else if (cardVal === 'Q') cardVal = 12;
else if (cardVal === 'J') cardVal = 11;
else if (cardVal === 'T') cardVal = 10;
else cardVal = parseInt(cardVal);
return {
cardVal: cardVal,
cardSuit: card.charAt(1)
}
})
.sortBy(function(cardInfo) {
return (-1) * cardInfo.cardVal;
})
.map(function(cardInfo) {
var cardValText = cardInfo.cardVal;
if (cardValText === 14) cardValText = 'A';
else if (cardValText === 13) cardValText = 'K';
else if (cardValText === 12) cardValText = 'Q';
else if (cardValText === 11) cardValText = 'J';
else if (cardValText === 10) cardValText = 'T';
return cardValText + cardInfo.cardSuit;
})
.value();
}
/**
* METHODS FOR RESOLVING ORDER BASED ON KICKERS
*
* This object contains methods for getting a value-number for
* a hand of some particular handRank (= value-order must rely on kickers)
* For example, 'resolveBetweenQuads' returns INT representing a relative value
* of that particular quads-hand, thus allowing two players both holding quads be compared
* against each other.
*/
var handComparisons = {
// Dispatcher
resolveBestKickerUsage: function(kickers, rank) {
// Should probably use a mapping of rank -> function instead here
// And convert those magic numbers into constants for god's sake.
if (rank === HIGH_CARD) return this.resolveBetweenHighCards(kickers);
if (rank === PAIR) return this.resolveBetweenPairs(kickers);
if (rank === TWO_PAIRS) return this.resolveBetweenTwoPairs(kickers);
if (rank === TRIPS) return this.resolveBetweenTrips(kickers);
if (rank === STRAIGHT) return this.resolveBetweenStraights(kickers);
if (rank === FLUSH) return this.resolveBetweenFlushes(kickers);
if (rank === FULL_HOUSE) return this.resolveBetweenFullHouses(kickers);
if (rank === QUADS) return this.resolveBetweenQuads(kickers);
if (rank === STRAIGHT_FLUSH) return this.resolveBetweenStraightFlushes(kickers);
if (rank === ROYAL_FLUSH) return this.resolveBetweenRoyalFlushes(kickers);
throw new Error("Resolving best kicker failed - no resolve method to call?");
},
// Individual resolvers between hands of same rank
resolveBetweenRoyalFlushes: function(kickers) {
// Well there can be just one or else all players share the board royal flush
return 1; // Only way to share royal flush is to have one on board
},
resolveBetweenStraightFlushes: function(kickers) {
// Simply sum up card values, highest sum is highest straight
return _.reduce(kickers, function(s, kicker) {
return s + kicker;
}, 0);
},
resolveBetweenQuads: function(kickers) {
var kickersToCounts = _.countBy(kickers, function(kicker) { return kicker});
var quadVal = 0;
var kickerVal = 0;
_.forOwn(kickersToCounts, function(count, kicker) {
if (count === 4) {
quadVal = kicker;
}
else if (count === 1) {
kickerVal = kicker;
}
});
return quadVal * 100 + kickerVal;
},
resolveBetweenFullHouses: function(kickers) {
var kickersToCounts = _.countBy(kickers, function(kicker) { return kicker});
var threeVal = 0;
var twoVal = 0;
_.forOwn(kickersToCounts, function(count, kicker) {
if (count === 3) {
threeVal = kicker;
}
else if (count === 2) {
twoVal = kicker;
}
});
return threeVal * 100 + twoVal;
},
resolveBetweenFlushes: function(kickers) {
return (kickers[0] * Math.pow(10, 8)
+ kickers[1] * Math.pow(10, 6)
+ kickers[2] * Math.pow(10, 4)
+ kickers[3] * Math.pow(10, 2)
+ kickers[4] * Math.pow(10, 0));
},
resolveBetweenStraights: function(kickers) {
// Simply sum up card values, highest sum is highest straight
return _.reduce(kickers, function(s, kicker) {
return s + kicker;
}, 0);
},
resolveBetweenTrips: function(kickers) {
var kickersToCounts = _.countBy(kickers, function(kicker) { return kicker});
var threeVal = 0;
var extras = [];
_.forOwn(kickersToCounts, function(count, kicker) {
if (count === 3) {
threeVal = kicker;
}
else if (count === 1) {
extras.push(kicker);
}
});
extras = extras.sort(function(a,b) {
return b-a;
})
return threeVal * 10000 + extras[0] * 100 + extras[1];
},
resolveBetweenTwoPairs: function(kickers) {
var kickersToCounts = _.countBy(kickers, function(kicker) { return kicker});
var twoPairFormingVals = [];
var extra = 0
_.forOwn(kickersToCounts, function(count, kicker) {
if (count === 2) {
twoPairFormingVals.push(kicker);
}
else if (count === 1) {
extra = kicker;
}
});
twoPairFormingVals = twoPairFormingVals.sort(function(a,b) {
return b-a;
})
return twoPairFormingVals[0] * 10000 + twoPairFormingVals[1] * 100 + extra;
},
resolveBetweenPairs: function(kickers) {
var kickersToCounts = _.countBy(kickers, function(kicker) { return kicker});
var pair = 0;
var extras = [];
_.forOwn(kickersToCounts, function(count, kicker) {
if (count === 2) {
pair = kicker;
}
else if (count === 1) {
extras.push(kicker);
}
});
extras = extras.sort(function(a,b) {
return b-a;
})
return (
pair * 1000000
+ extras[0] * 10000
+ extras[1] * 100
+ extras[2] * 1
)
},
resolveBetweenHighCards: function(kickers) {
return (kickers[0] * Math.pow(10, 8)
+ kickers[1] * Math.pow(10, 6)
+ kickers[2] * Math.pow(10, 4)
+ kickers[3] * Math.pow(10, 2)
+ kickers[4] * Math.pow(10, 0));
},
}
// PUBLIC API METHOD
function valueOfHand(boardCards, holeCards, includeHandRank) {
// Concat hole cards and board cards together
var cardsToUse = _.flatten(_.concat(boardCards, holeCards));
// Generate all possible five card combos
var combos = createAllFiveCombos(cardsToUse);
// Evaluate each combo
var evals = _.map(combos, function(combo) {
return {
combo: combo,
evaluation: evaluateCombo(combo)
}
})
//console.log("--Evals--");
//console.log(evals);
// Find out best handRank within the numerous evaluation objects
var bestRank = 10;
_.forEach(evals, function(evalInfo) {
if (bestRank > evalInfo.evaluation.handRank) bestRank = evalInfo.evaluation.handRank;
})
//console.log("--Best rank found: " + bestRank);
// Only remain those which have the best handRank
var bests = _.filter(evals, function(evalInfo) {
return evalInfo.evaluation.handRank === bestRank;
})
//console.log("--Best evals--");
//console.log(bests);
var best = bests.length === 1 ? bests[0] : findOutBestByKickers(bests);
// Continue from here
// Next differentiate between combos with same handRank but different kickers!
var o = {
cards: rankCards(best.combo),
handType: best.evaluation.handType,
kickers: best.evaluation.kickers
}
if (includeHandRank) o.handRank = best.evaluation.handRank;
return o;
}
// PUBLIC API METHOD
function rankEvaluations(evals) {
// Resolve best winning hand type
var bestRank = 10;
_.forEach(evals, function(evalObj) {
if (bestRank > evalObj.evalInfo.handRank) bestRank = evalObj.evalInfo.handRank;
})
//console.log("--Best rank found: " + bestRank);
// Only remain those which have the best handRank
var bests = _.filter(evals, function(evalObj) {
return evalObj.evalInfo.handRank === bestRank;
});
// Resolve among winning hand types the one with best kickers
var bestEvalInfos =_.chain(bests)
.map(function(evalObj) {
var kickers = evalObj.evalInfo.kickers;
return {
id: evalObj.id,
kickersWorth: handComparisons.resolveBestKickerUsage(kickers, bestRank),
evalInfo: evalObj.evalInfo
}
})
.sortBy(function(summedEvaluation) {
return (-1) * summedEvaluation.kickersWorth;
})
.value();
// Check if multiple hands are drawing the best kickers
var checkedAgainst = bestEvalInfos[0];
return _.filter(bestEvalInfos, function(evalInfo) {
return evalInfo.kickersWorth === checkedAgainst.kickersWorth;
})
}
module.exports = {
// Test interface
getCombos: function(boardCards, holeCards) {
var cardsToUse = _.flatten(_.concat(boardCards, holeCards));
var allCombos = createAllFiveCombos(cardsToUse);
return _.map(allCombos, function(combo) {
return _.sortBy(combo, function(card) {
return card;
})
})
},
/*
* Resolves the value of the hand
* @param {Array} boardCards - The five shared board cards
* @param {Array} holeCards - The two private hole cards of the player
* @returns {Object} - Object containing info about the value of the hand
*/
valueOfHand: valueOfHand,
/*
* Resolves the ranking between multiple hands
* @param {Array} boardCards - The five shared board cards
* @param {Array} arrayOfHoleCards - Array of holecard arrays
* @returns {Array} - Array where holecards are ranked in order (most valued 1st)
*/
getWinners: function(boardCards, arrayOfHoleCardObjects) {
// holeCardObject = {id (optional): INT/STRING, cards: ARRAY[2]}
var evals = _.map(arrayOfHoleCardObjects, function(holeCardObject) {
var holeCards = holeCardObject.cards;
return {
id: holeCardObject.id,
evalInfo: valueOfHand(boardCards, holeCards, true)
}
});
//console.log("RANK HAND EVALS");
//console.log(evals);
var bests = rankEvaluations(evals);
//console.log("BEST RANKED HAND");
//console.log(bests);
return bests;
},
/*
* Resolves the ranking between multiple eval-objects (as returned by valueOfHand)
* @param {Array} evals - Array of evaluation objects
* @returns {Array} - Array where evaluations are ranked best first
*/
rankEvaluations: rankEvaluations
}