From 7c3a1378f98306de6c0879395944d00cb31d4efd Mon Sep 17 00:00:00 2001 From: James Brumond Date: Tue, 18 Sep 2018 00:04:40 -0700 Subject: [PATCH] WIP for poker hand evaluation --- src/hands/compare.js | 4 + src/hands/evaluator.js | 164 +++++++++++++++++++++++++++++ src/hands/hand.js | 31 ++++++ src/hands/index.js | 14 +++ src/hands/ranking.js | 53 ++++++++++ src/hands/straights-and-flushes.js | 0 src/hands/wild-cards.js | 43 ++++++++ src/hands/x-of-a-kind.js | 75 +++++++++++++ 8 files changed, 384 insertions(+) create mode 100644 src/hands/compare.js create mode 100644 src/hands/evaluator.js create mode 100644 src/hands/hand.js create mode 100644 src/hands/index.js create mode 100644 src/hands/ranking.js create mode 100644 src/hands/straights-and-flushes.js create mode 100644 src/hands/wild-cards.js create mode 100644 src/hands/x-of-a-kind.js diff --git a/src/hands/compare.js b/src/hands/compare.js new file mode 100644 index 0000000..0e7cfde --- /dev/null +++ b/src/hands/compare.js @@ -0,0 +1,4 @@ + +exports.compare = (ranking, handA, handB) => { + // +}; diff --git a/src/hands/evaluator.js b/src/hands/evaluator.js new file mode 100644 index 0000000..899f992 --- /dev/null +++ b/src/hands/evaluator.js @@ -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.[]} + */ + +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.} + * @param bugCardSet {Set.} + * @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., cardsBySuit: Map. }} + */ +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.} + * @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); + } +} diff --git a/src/hands/hand.js b/src/hands/hand.js new file mode 100644 index 0000000..2b6b5c4 --- /dev/null +++ b/src/hands/hand.js @@ -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 + }); + } +}; diff --git a/src/hands/index.js b/src/hands/index.js new file mode 100644 index 0000000..de763f8 --- /dev/null +++ b/src/hands/index.js @@ -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 + } +}; diff --git a/src/hands/ranking.js b/src/hands/ranking.js new file mode 100644 index 0000000..55ac018 --- /dev/null +++ b/src/hands/ranking.js @@ -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); +}; diff --git a/src/hands/straights-and-flushes.js b/src/hands/straights-and-flushes.js new file mode 100644 index 0000000..e69de29 diff --git a/src/hands/wild-cards.js b/src/hands/wild-cards.js new file mode 100644 index 0000000..d92ceda --- /dev/null +++ b/src/hands/wild-cards.js @@ -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; +}; diff --git a/src/hands/x-of-a-kind.js b/src/hands/x-of-a-kind.js new file mode 100644 index 0000000..db2dc3a --- /dev/null +++ b/src/hands/x-of-a-kind.js @@ -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) => { + // + }); +};