This is a follow-up article relating to Lets build a deck of cards in JavaScript, we want to use that module in a game of Poker. We expect to pass an array of cards through a scoring algorithm which then returns a value we can compare against other hands that are in play.
Scoring poker hands isn't the most intuitive thing in the world, I'll do my best to explain what we are doing and why. But I cannot guarantee it will be easy.
First lets look at our interfaces.
interface Card
{
suit: string;
rank: string;
}
interface PokerHandResult
{
cards: Card[];
name: string;
value: number;
}
Cards are identical to our cards from the Deck module, except we never need to worry about any construction parameters. Poker hands are agnostic about suits, but necessitate valid poker ranks in order to work.
Lets create a score
method which will serve as the endpoint to our code.
namespace PokerHand
{
// ranks in order
var _ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'jack', 'queen', 'king', 'ace'];
export function score (...allCards: Card[][]): PokerHandResult
{
// return the best poker hand from a set or sets of cards
let cards = _sanitise(allCards);
// start empty
let best = _result(cards);
// find best hand
for (let combination of _combinations(cards, 5)) {
// calculate value of 5 cards
let result = _calculate(combination);
if (result.value > best.value)
best = result;
}
// finish with best result
return best;
}
}
Valid poker ranks are listed at the top of the namespace in order of value. This is important in order to determine the best hand.
By default we say we are returning a result of all cards with a scoring value of 0. As long as there is at least one set of 5 valid cards which were provided to the algorithm we can assume that will change. As even "high card" has a value above 0.
We go through every possible combination of five cards, score them, then return the best hand of five cards.
function _result (cards: Card[], name?: string, value?: number): PokerHandResult
{
return {
cards: cards,
name: name || 'nothing',
value: value || 0
};
}
The purpose here is to return an object formatted as one of our results, with defaults built in. Where an absent name and value would simply be filled in for us. We use this in three different places in our code so it's nice to have it as a function.
function _sanitise (allCards: Card[][]): Card[]
{
// concatenate
let cards: Card[] = [].concat.apply([], allCards);
// valid rank and suit
cards = cards.filter((card) => {
return !!(_ranks.indexOf(card.rank) > -1 && card.suit);
});
return cards;
}
We produce a single sanitised array of cards from whatever was passed into the method. A valid card has any suit accompanied by a valid poker rank.
function _combinations (cards: Card[], groups: number): Card[][]
{
// card combinations with the given size
let result: Card[][] = [];
// not enough cards
if (groups > cards.length)
return result;
// one group
if (groups == cards.length)
return [cards];
// one card in each group
if (groups == 1)
return cards.map((card) => [card]);
// everything else
for (let i = 0; i < cards.length - groups; i++) {
let head = cards.slice(i, (i + 1));
let tails = _combinations(cards.slice(i + 1), (groups - 1));
for (let tail of tails) {
result.push(head.concat(tail));
}
}
return result;
}
Here we have a fancy way of splitting the array up into sets of as many cards as we want. In the case of our score
method we always want full sets of 5 cards. But it's important to be able to return sets of sizes other than that.
As you can see in the "everything else" section we re-use this method to return ever smaller groups. The output is eventually, beautifully, every possible combination of 5 cards.
function _ranked (cards: Card[]): Card[][]
{
// split cards by rank
let result: Card[][] = [];
for (let card of cards) {
let r = _ranks.indexOf(card.rank);
result[r] = result[r] || [];
result[r].push(card);
}
// condense
result = result.filter((rank) => !!rank);
// high to low
result.reverse();
// pairs and sets first
result.sort((a, b) => {
return a.length > b.length ? -1 : a.length < b.length ? 1 : 0;
});
return result;
}
We instantiate a two dimensional array of cards. We look at each card and place it into it's appropriate row in the array using it's index from the _ranks
variable. If there were two of the same rank in the hand for example they would be in the same row as one another.
We then eliminate all empty rows in the array, so that each row contains at least one card. The array is reversed so that high ranking cards appear first.
We then rearrange the rows again so that rows with more cards come first.
function _isFlush (cards: Card[]): boolean
{
// all suits match is flush
let suit = cards[0].suit;
for (let card of cards) {
if (card.suit != suit)
return false;
}
return true;
}
Check if all cards have a matching suit.
function _isStraight (ranked: Card[][]): boolean
{
// must have 5 different ranks
if (!ranked[4])
return false;
// could be wheel if r0 is 'ace' and r4 is '2'
if (ranked[0][0].rank == 'ace' && ranked[1][0].rank == '5' && ranked[4][0].rank == '2') {
// hand is 'ace' '5' '4' '3' '2'
ranked.push(ranked.shift());
// ace is now low
return true;
}
// run of five in row is straight
let r0 = _ranks.indexOf(ranked[0][0].rank);
let r4 = _ranks.indexOf(ranked[4][0].rank);
return (r0 - r4) == 4;
}
If ranked[4]
exists it means we have 5 cards with different ranks.
Straights are a little bit funny because there is such a thing as "wheel" meaning that the ace can be low. If we have five cards all in sequence that is a straight, but we need to re-arrange the ranked
array again if the ace is low.
The only way "wheel" is possible is if the high card in your hand is ace
and the low card is 2
. So we determine whether that is the case. If it is we check if the next highest card is 5
in which case we know our hand is the sequence ace
, 5
, 4
, 3
, 2
.
We need to remove the first row and move it to the back, giving it a lower value. That first row is our highest rank, so it would be the ace. Because in JavaScript we are always working with references to objects we can make changes to the ranked
variable and expect the changes will be reflected in other places.
Otherwise, we simply subtract the index of the first rank from the index of the last rank to find the number 4
and know we have a straight.
function _value (ranked: Card[][], primary: number): number
{
// primary wins the rest are kickers
let str = '';
for (let rank of ranked) {
// create two digit value
let r = _ranks.indexOf(rank[0].rank);
let v = (r < 10 ? '0' : '') + r;
for (let i = 0; i < rank.length; i++) {
// append value for each card
str += v;
}
}
// to integer
return (primary * 10000000000) + parseInt(str);
}
It is possible two people will have for example "two pair" but with a different kicker, or different high card. So we need to score all of the cards in the hand and reflect their value in the result. We represent each card as a two digit number and to do this most easily we are using a string.
A card with a rank of 5
for example would be looked up in _ranks
as an index and return a value of 3
. Since 3
is less than 10 we prepend a 0
so that it comes out as 03
, for each card with this same rank we append it again.
Then convert the string into an integer and add it to the most important number which is the value of our poker hand. We add a bunch of zeros, the number of zeros we expect the string to be in length.
function _calculate (cards: Card[]): PokerHandResult
{
// determine value of hand
let ranked: Card[][] = _ranked(cards);
let isFlush = _isFlush(cards);
let isStraight = _isStraight(ranked);
if (isStraight && isFlush && ranked[0][0].rank == 'ace')
return _result(cards, 'royal flush', _value(ranked, 9));
else if (isStraight && isFlush)
return _result(cards, 'straight flush', _value(ranked, 8));
else if (ranked[0].length == 4)
return _result(cards, 'four of a kind', _value(ranked, 7));
else if (ranked[0].length == 3 && ranked[1].length == 2)
return _result(cards, 'full house', _value(ranked, 6));
else if (isFlush)
return _result(cards, 'flush', _value(ranked, 5));
else if (isStraight)
return _result(cards, 'straight', _value(ranked, 4));
else if (ranked[0].length == 3)
return _result(cards, 'three of a kind', _value(ranked, 3));
else if (ranked[0].length == 2 && ranked[1].length == 2)
return _result(cards, 'two pair', _value(ranked, 2));
else if (ranked[0].length == 2)
return _result(cards, 'one pair', _value(ranked, 1));
else
return _result(cards, 'high card', _value(ranked, 0));
}
Here we harvest the fruits of our labour. The value of cards
at this stage we can expect is 5 cards with a valid poker rank each.
There are two booleans which will serve to tell us whether or not a straight or flush were detected. Now it's the easy part we go through each possible hand in order from highest value to lowest until we find a match.
The result we are returning now contains the 5 cards we were evaluating, the name of the hand, and a value representing its strength in relation to any other 5 card combination.
When running the score
method pass as many arrays of cards as you like at once. This is useful when there is a common pool of shared cards. In the case of Texas Holdem for instance, you could pass the player's two pocket cards along with the communal cards from the middle.
var module = <any>module;
if (typeof module == "object" && typeof module.exports == "object")
module.exports = PokerHand;
In order to make it possible to run tests we append an export statement to our code.