cactus-josh
Version:
An implementation of the CactusKev poker hand evaluator
126 lines (105 loc) • 5.85 kB
JavaScript
// Here's how a card is represented in Cactus:
// +--------+--------+--------+--------+
// |xxxbbbbb|bbbbbbbb|cdhsrrrr|xxpppppp|
// +--------+--------+--------+--------+
// p = prime number of rank (2 to 41)
// r = rank of card (2 to 14)
// cdhs = bit set for suit of card
// b = bit set for rank of card
// Import lookup tables
const lookupTables = require( "./lookupTables" );
// const primeLookup = require( "./primeMultiplicands" );
// Note how suits are represented as set bits
exports.suits = { 8: "Clubs", 4: "Diamonds", 2: "Hearts", 1: "Spades" };
// Let's define each rank (2 to 14/Ace) as a prime number
exports.rankPrimes = [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41 ];
// And a function for a card's rank, 2 to 14/Ace
exports.rank = card => ( card >>> 8 ) % 16;
// And another for its suit
exports.suit = card => ( card >>> 12 ) % 16;
// Now some arrays & a corresponding function for suit/rank/card name strings
// Note the irregular spacing to correspond to how we're representing suits as set bits [ 1, 2, 4, 8 ]
exports.rankNames = [ "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Jack", "Queen", "King", "Ace" ];
exports.suitNames = [ null, "Spades", "Hearts", null, "Diamonds", null, null, null, "Clubs" ];
exports.cardName = card => `${ this.rankNames[ this.rank( card ) ] } of ${ this.suitNames[ this.suit( card ) ] }`;
// This is a clever fast way to count bits in an integer courtesy of Sean Eron Anderson
// https://graphics.stanford.edu/~seander/bithacks.html
exports.countBits = bit => {
const counter = bit - ( ( bit >>> 1 ) & 3681400539 ) - ( ( bit >>> 2 ) & 1227133513 );
return ( ( counter + ( counter >>> 3 ) ) & 3340530119 ) % 63;
}
// Now here's a function to compile a full deck
// To shuffle, pass anything other than null/undefined/0/NaN/"" as a parameter
exports.fullDeck = shuffled => {
const result = [];
for ( let rank = 0; rank < 13; rank++ ) for ( let suit of [ 8, 4, 2, 1 ] )
result.push( ( this.rankPrimes[ rank ] ) | ( rank << 8 ) | ( suit << 12 ) | ( ( 1 << rank ) << 16 ) );
if ( !shuffled ) return result;
for ( let i = 51; i > 0; i-- ) {
const j = Math.floor( Math.random() * ( i + 1 ) );
[ result[ i ], result[ j ] ] = [ result[ j ], result[ i ] ];
}
return result;
}
// When representing cards this way, bitwise-and-ing everything with 61,440 will result in a 0
// if the hand is not a flush - this is the same as:
// hand => hand[ 0 ] & hand[ 1 ] & hand[ 2 ] & hand[ 3 ] & hand[ 4 ] & 0xF000;
exports.flush = hand => hand.reduce( ( total, card ) => total & card, 0xF000 );
// Here's where it gets interesting
// If a hand is a flush, then bitwise-or-ing everything and shifting it all 16 bits to the right
// will result in a number with exactly five set bits (one for each card) - these are all unique and
// they correspond to a lookup table, flushes[], with the value for each
exports.flushBitPattern = flush => flush.reduce( ( total, card ) => total | card , 0 ) >>> 16;
exports.flushRank = flush => lookupTables.flushes[ this.flushBitPattern( flush ) ];
// if the hand isn't a flush or straight flush, let's use a different lookup table to check
// for straights
exports.fiveUniqueCardsRank = hand => lookupTables.fiveUniqueCards[ this.flushBitPattern( hand ) ];
// We've eliminated flushes, straights and high-card hands – let's move on to pairs & threes
// Since we're representing each rank as a prime, the multiplicand of all rank primes together
// is guaranteed to be unique
exports.primeMultiplicand = hand => hand.reduce( ( total, card ) => total * ( card & 0xFF ), 1 );
// This multiplicand will be way too large for a lookup table so instead we'll speed things up
// to log-n time with this perfect hash lookup function - courtesy of Paul Senzee
// http://senzee.blogspot.com/2006/06/some-perfect-hash.html
exports.findFast = u => {
u += 0xe91aaa35;
u ^= u >>> 16;
u += u << 8;
u ^= u >>> 4;
let a = ( u + ( u << 2 ) ) >>> 19;
return a ^ lookupTables.hashAdjust[ ( u >>> 8 ) & 0x1ff ];
};
// Finally let's tie it all together - first check for flushes, then straights, then pairs/threes
exports.handRank = hand => {
if ( this.flush( hand ) ) return this.flushRank( hand );
let fiveUniqueCardsRank = this.fiveUniqueCardsRank( hand );
if ( fiveUniqueCardsRank ) return fiveUniqueCardsRank;
return lookupTables.hashValues[ this.findFast( this.primeMultiplicand( hand ) ) ];
};
// Some quick simple branching to return the hand's rank as a string
exports.handValue = hand => {
const rank = this.handRank( hand );
if ( rank > 6185 ) return "High card";
else if ( rank > 3325 ) return "One pair";
else if ( rank > 2467 ) return "Two pair";
else if ( rank > 1609 ) return "Three of a kind";
else if ( rank > 1599 ) return "Straight";
else if ( rank > 322 ) return "Flush";
else if ( rank > 166 ) return "Full house";
else if ( rank > 10 ) return "Four of a kind";
else return "Straight flush";
};
// A function to generate possible hands (k-combinations)
// https://medium.com/nerd-for-tech/july-2-generating-k-combinations-with-recursion-in-javascript-71ef2b90b44b
exports.possibleHands = ( deck, combinationLength ) => {
let head, tail, result = [];
if ( combinationLength > deck.length || combinationLength < 1 ) { return []; }
if ( combinationLength === deck.length ) { return [ deck ]; }
if ( combinationLength === 1 ) { return deck.map( element => [ element ] ); }
for ( let i = 0; i < deck.length - combinationLength + 1; i++ ) {
head = deck.slice( i, i + 1 );
tail = this.possibleHands( deck.slice( i + 1 ), combinationLength - 1 );
for ( let j = 0; j < tail.length; j++ ) { result.push( head.concat( tail[ j ] ) ); }
}
return result;
}