Skip to content

Poker hand evaluation #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/hands/compare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

exports.compare = (ranking, handA, handB) => {
//
};
164 changes: 164 additions & 0 deletions src/hands/evaluator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@

const { Hand } = require('./hand');
const { Card } = require('../card');
const { standardHandRanking } = require('./ranking');

/**
* @typedef HandEvaluatorOptions
* @property wilds {Card[]}
* @property bugs {Card[]}
* @property aces {'low'|'high'|'split'|'low-or-high'}
* @property handRanking {Function.<Hand>[]}
*/

const props = new WeakMap();

/**
* Used to evaluate a set of cards for poker hands
*
* wilds - A list of all cards that should be treated as full wild cards
* bugs - A list of all cards that should be treated as [bug cards](https://en.wikipedia.org/wiki/Bug_\(poker\))
* aces - The rule for Ace ranking; "high", "low", "high-or-low", or "split"
* handRanking - An array of Hand classes, in order from best to worst, to determine what the best hand is
*
* @class HandEvaluator
*/
exports.HandEvaluator = class HandEvaluator {
constructor(/** @type {HandEvaluatorOptions} */ { wilds = [ ], bugs = [ ], aces = 'low-or-high', handRanking = standardHandRanking }) {
props.set(this, {
wilds: new Set(wilds),
bugs: new Set(bugs),
aces,
handRanking
});
}

evaluateHand(cards) {
const { wilds, bugs, aces, handRanking } = props.get(this);
const { naturalCards, wildCards, bugCards } = separateWildCards(cards, wilds, bugs);

const { cardsByRank, cardsBySuit } = separateCardsByRankAndSuit(naturalCards);
const { groupsBySize } = findRankGroupings(cardsByRank);

const handDetails = new HandDetail({ cards, naturalCards, wildCards, bugCards, cardsByRank, cardsBySuit, groupsBySize, aces });

let hand;

handRanking.some((HandType) => {
hand = HandType.find(handDetails);

if (hand) {
return true;
}
});

return hand;
}
};

/**
* Separates out a hand of cards into 3 lists: natural cards, wild cards, and bug cards
*
* @param cards {Card[]}
* @param wildCardSet {Set.<Card>}
* @param bugCardSet {Set.<Card>}
* @return {{ naturalCards: Card[], wildCards: Card[], bugCards: Card[] }}
*/
const separateWildCards = (cards, wildCardSet, bugCardSet) => {
let wildCards = [ ];
let bugCards = [ ];

const naturalCards = cards.slice();

for (let i = 0; i < naturalCards.length; i++) {
const card = naturalCards[i];

if (wildCardSet.has(card)) {
wildCards.push(card);
naturalCards.splice(i--, 1);
}

else if (bugCardSet.has(card)) {
bugCards.push(card);
naturalCards.splice(i--, 1);
}
}

return {
naturalCards,
wildCards,
bugCards
};
};

/**
* Separates cards into buckets by rank
*
* @param cards {Card[]}
* @return {{ cardsByRank: Map.<Rank, Card[]>, cardsBySuit: Map.<Rank, Card[]> }}
*/
const separateCardsByRankAndSuit = (cards) => {
const cardsByRank = new Map();
const cardsBySuit = new Map();

cards.forEach((card) => {
const { rank, suit } = card;

if (! cardsByRank.has(rank)) {
cardsByRank.set(rank, [ ]);
}

if (! cardsBySuit.has(suit)) {
cardsBySuit.set(suit, [ ]);
}

cardsByRank.get(rank).push(card);
cardsBySuit.get(suit).push(card);
});

return { cardsByRank, cardsBySuit };
};

/**
* Organizes cards by the number of cards for that rank that exists (ie. it finds pairs, three of a kinds, etc)
*
* @param naturalCardsByRank {Map.<Rank, Card[]>}
* @return {{ groupsBySize: { [size: number]: Card[][] } }}
*/
const findRankGroupings = (naturalCardsByRank) => {
const groupsBySize = {
'5': [ ],
'4': [ ],
'3': [ ],
'2': [ ]
};

naturalCardsByRank.forEach((cards/*, rank*/) => {
if (cards.length > 1) {
groupsBySize[cards.length].push(cards);
}
});

return { groupsBySize };
};

/**
* Contains some general metadata about a hand of cards, used for determining the value
* of the hand.
*
* @class HandDetail
*/
class HandDetail {
constructor({ cards, naturalCards, wildCards, bugCards, cardsByRank, cardsBySuit, groupsBySize, aces }) {
this.cards = cards;
this.naturalCards = naturalCards;
this.wildCards = wildCards;
this.bugCards = bugCards;
this.cardsByRank = cardsByRank;
this.cardsBySuit = cardsBySuit;
this.groupsBySize = groupsBySize;
this.aces = aces;

Object.freeze(this);
}
}
31 changes: 31 additions & 0 deletions src/hands/hand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

const { findHighCard } = require('./ranking');

/**
* @typedef HandOptions
* @property cards {Card[]}
* @property rank {rank|rank[]=}
* @property highCard {Card=}
*/

exports.Hand = class Hand {
constructor(/** @type {HandOptions} */ { cards, rank, highCard, acesHigh }) {
this.cards = cards;
this.highCard = highCard || findHighCard(cards, acesHigh);
this.rank = rank;

Object.freeze(this);
}
};

exports.HighCard = class HighCard extends exports.Hand {
constructor({ cards, acesHigh }) {
const highCard = findHighCard(cards, acesHigh);

super({
cards,
highCard,
rank: highCard.rank
});
}
};
14 changes: 14 additions & 0 deletions src/hands/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

const { Hand } = require('./hand');
const { jokers, deuces, oneEyedJacks, suicidalKings } = require('./wild-cards');
const { FiveOfAKind, FourOfAKind, FullHouse, ThreeOfAKind, TwoPair, Pair } = require('./x-of-a-kind');
const { standardHandRanking } = require('./ranking');

module.exports = {
Hand,
standardHandRanking,
wilds: { jokers, deuces, oneEyedJacks, suicidalKings },
hands: {
FiveOfAKind, FourOfAKind, FullHouse, ThreeOfAKind, TwoPair, Pair
}
};
53 changes: 53 additions & 0 deletions src/hands/ranking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

const { max, indexArray } = require('../utils');
const { HighCard } = require('./hand');
const { StraightFlush, Flush, Straight } = require('./straights-and-flushes');
const { FiveOfAKind, FourOfAKind, FullHouse, ThreeOfAKind, TwoPair, Pair } = require('./x-of-a-kind');
const { Rank } = require('../rank');
const { ace, two, three, four, five, six, seven, eight, nine, ten, jack, queen, king } = require('../ranks');

exports.acesLowRankOrder = indexArray([ ace, two, three, four, five, six, seven, eight, nine, ten, jack, queen, king ]);
exports.acesHighRankOrder = indexArray([ two, three, four, five, six, seven, eight, nine, ten, jack, queen, king, ace ]);

// TODO - Find a way to rank wild cards as the card they are actually representing

exports.findHighCard = (cards, acesHigh) => {
const ranks = acesHigh
? exports.acesHighRankOrder
: exports.acesLowRankOrder;

return max(cards, (card) => getRank(card, ranks));
};

exports.compareCards = (cardA, cardB, acesHigh) => {
const ranks = acesHigh
? exports.acesHighRankOrder
: exports.acesLowRankOrder;

return getRank(cardA, ranks) - getRank(cardB, ranks);
};

exports.standardHandRanking = Object.freeze([
FiveOfAKind,
StraightFlush,
FourOfAKind,
FullHouse,
Flush,
Straight,
ThreeOfAKind,
TwoPair,
Pair,
HighCard
]);

// exports.lowballHandRanking = Object.freeze([
// //
// ]);

const getRank = (cardOrRank, ranks) => {
if (cardOrRank instanceof Rank) {
return ranks.get(cardOrRank);
}

return ranks.get(cardOrRank.rank);
};
Empty file.
43 changes: 43 additions & 0 deletions src/hands/wild-cards.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

const { hearts, spades } = require('../suits');
const { joker, two, jack, king } = require('../ranks');

/**
* Locates joker cards, used for jokers wild rules
*
* @param card {Card}
* @return {boolean}
*/
exports.jokers = (card) => {
return card.rank === joker;
};

/**
* Locates cards with a rank of two, used for deuces wild rules
*
* @param card {Card}
* @return {boolean}
*/
exports.deuces = (card) => {
return card.rank === two;
};

/**
* Locates one-eyed jacks (jack of hearts or spades), used for one-eyed jacks wild rules
*
* @param card {Card}
* @return {boolean}
*/
exports.oneEyedJacks = (card) => {
return card.rank === jack && (card.suit === hearts || card.suit === spades);
};

/**
* Locates suicidal kings (king of hearts), used for suicidal kings wild rules
*
* @param card {Card}
* @return {boolean}
*/
exports.suicidalKings = (card) => {
return card.rank === king && card.suit === hearts;
};
75 changes: 75 additions & 0 deletions src/hands/x-of-a-kind.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@

const { Hand } = require('./hand');
const { ace, king } = require('../ranks');
const { acesLowRankOrder, acesHighRankOrder, findHighCard } = require('./ranking');

exports.FiveOfAKind = class FiveOfAKind extends Hand {
static find({ wildCards, bugCards, groupsBySize, aces }) {
const acesHigh = aces !== 'low';

let found;
const ranks = acesHigh ? acesHighRankOrder : acesLowRankOrder;

// If you've got five wild cards, you've automatically got five of a kind
if (wildCards.length + bugCards.length >= 5) {
// If aces are low, and you've got enough full wilds, go five of a kind kings
if (! acesHigh && wildCards.length >= 5) {
const cards = [ ...wildCards ];

cards.length = 5;

found = new FiveOfAKind({
cards,
rank: king,
highCard: cards[0]
});
}

// Otherwise, go five of a kind aces
else {
const cards = [ ...bugCards, ...wildCards ];

cards.length = 5;

found = new FiveOfAKind({
cards,
rank: ace,
highCard: cards[0]
});
}
}

groupsBySize[5].forEach((cards) => {
const rank = cards[0].rank;
const rankIndex = ranks.indexOf(rank);

// If this five of a kind is better than any we've previously seen, take it
if (! found || rankIndex >= ranks.indexOf(found.rank)) {
//
}
});
}
};

const findHighestRank = (cards, acesHigh) => {
//
};












exports.findLargestRankGroup = (naturalCardsByRank, wildCards, aces) => {
let largestGroup;

naturalCardsByRank.forEach((cards, rank) => {
//
});
};