5
\$\begingroup\$

I'm building a poker Hold'em hand evaluator. Ultimately my goal after building the evaluator is to build a client on the browser but that's far from now.

What I've done so far is a hand analyzer that check checks hand strength but I'm unsure of the architecture I've chosen. Since this is going to be at the core of what I'll build after I'd like to make the right decision from the get go.

Here is what is included:

  • Card: A card has a Suit (diamond) and a Rank ( ace).
  • Hand: A hand has cards[]. A hand actually represent the cards the player is holding + the cards on the board (which means that a hand has between 0 and n cards). A hand also has ranks[][] so when a 2 is placed in it it's placed at ranks[0].push(2). It makes it easy to see if there are four of a kind, pairs, and sets.
  • HandEvaluator: evaluate a hand and return a WinningHand
  • WinningHand: representation of the winning hand with its strength.

So what I'm interested in being reviewed on is:

  • The architecture, class names I've chosen. Why something else would make more sense?
  • If you have time I'd like the logic for evaluating a hand to be reviewed as well. I think it's a bit mind bending with the filterings etc. that happen.

Demo here .. all the files are under ./src/poker/


Card

export class Card {

    private static suits: string[] = [ 'h', 's', 'd', 'c' ];
    private static ranks: string[] = [ '2', '3', '4', '5', '6', '7', '8', '9', 'T',
                                                                        'J', 'Q', 'K', 'A' ];

    constructor(private _rank: Rank, private _suit: Suit) {
        if (this._rank >= Card.ranks.length)
            throw Error('A rank must be 0 and 12. ( Ace to king)');
        if (this._suit >= Card.suits.length)
            throw Error('A suit must be between 0 and 3 (h, s, d, c)')
    }

    toString() {
        return Card.ranks[this._rank] + Card.suits[this._suit];
    }

    static sortFn(a: Card, b: Card) {
        return b.rank - a.rank;
    }

    get suit() {
        return this._suit;
    }

    get rank() {
        return this._rank;
    }

    // transfor 4d, 7s type of strings to a new Card
    static fromString(str: string) {
        if (str.length !== 2)
            throw Error('Card length should be 2');

        const rank = Card.ranks.findIndex(x => x === str[0]);
        const suit = Card.suits.findIndex(x => x === str[1]);

        if (rank === -1 || suit === -1)
            throw Error(`Rank ${str[0]} or suit ${str[1]} was not found`);

        return new Card(rank, suit);
    }
}

Hand:

// the Hand class contains the player cards + board cards.
export class Hand {
    public cards: Card[] = [];
    // those two are used when evaluating a hand
    // take a look at evaluate hand for more details
    public suits: Card[][] = new Array(4).fill(0).map(_ => []);
    public ranks: Card[][] = new Array(13).fill(0).map(_ => []);

    constructor(cards: Card[] = []) {
        cards.forEach(card => this.addCard(card));
    }

    addCard(card: Card): Hand {
        if (this.cards.length > 7)
            throw Error('Hand containing board card must have max 7 cards. This is Hold\'em');
        // precautious check to see that we can actually add the card (there shouldn't be two Ad in a deck for example)
        if (this.cards.some(c => c.toString() === card.toString()))
            throw Error('This card has already been added to this hand!');

        this.cards.push(card);
        // we are adding the suit and ranks to their respective array so we can easily
        // evaluate those.
        this.suits[card.suit].push(card);
        this.ranks[card.rank].push(card);
        return this;
    }

    evaluateHand() {
        // in hold'em poker there is 5 cards on board + 2 cards in hand.
        // for convenience, the Hand class contains the board cards as well
        if (this.cards.length < 7)
            throw new Error('When evaluating a hand, the hand must have 7 cards');

    }

    hasRank(rank: Rank) {
        return this.ranks[rank].length > 0;
    }

    // gives back a string representation of the hand of the form: 7d 6h 8s Js 9s Td As
    toString() {
        return this.cards.toString().replace(/,/g, ' ');
    }

    static fromString(str: String): Hand {
        const hand = new Hand();
        const cardsStr = str.split(' ');
        cardsStr.forEach(cardStr => {
            const card = Card.fromString(cardStr);
            hand.addCard(card);
        });
        return hand;
    }
}

Hand Evaluator

export class HandEvaluator {

    evaluateHand(hand: Hand): WinningHand {
        let winningCards: Card[] | undefined;
        if (winningCards = this.evaluateRoyalFlush(hand))
            return new WinningHand(winningCards, HandRank.ROYAL_FLUSH);
        if (winningCards = this.evaluateStraightFlush(hand))
            return new WinningHand(winningCards, HandRank.STRAIGHT_FLUSH);
        else if (winningCards = this.evaluateFourOfAKind(hand))
            return new WinningHand(winningCards, HandRank.FOUR_OF_A_KIND);
        else if (winningCards = this.evaluateFullHouse(hand))
            return new WinningHand(winningCards, HandRank.FULL_HOUSE);
        else if (winningCards = this.evaluateFlush(hand))
            return new WinningHand(winningCards, HandRank.FLUSH);
        else if (winningCards = this.evaluateStraight(hand))
            return new WinningHand(winningCards, HandRank.STRAIGHT);
        else if (winningCards = this.evaluateSet(hand))
            return new WinningHand(winningCards, HandRank.SET);
        else if (winningCards = this.evaluateDoublePair(hand))
            return new WinningHand(winningCards, HandRank.DOUBLE_PAIR);
        else if (winningCards = this.evaluatePair(hand))
            return new WinningHand(winningCards, HandRank.PAIR);
        else
            return new WinningHand(this.findHighests(5, hand.cards), HandRank.HIGH_CARD);
    }

    evaluateRoyalFlush(hand: Hand): Card[] | undefined {
        const straightFlush = this.evaluateStraightFlush(hand);
        if (straightFlush) {
            const sfHand = new Hand(straightFlush);
            if (sfHand.hasRank(Rank.ACE) && sfHand.hasRank(Rank.KING))
                return sfHand.cards;
        }
    }

    evaluateStraightFlush(hand: Hand): Card[] | undefined {
        let flush: any = this.evaluateFlush(hand, 7);
        let straightFlush;
        if (flush) {
            straightFlush = this.evaluateStraight(new Hand(flush));
        }
        return straightFlush;
    }

    // returns the biggest flush in a Hand
    evaluateFlush(hand: Hand, amount: number = 5): Card[] | undefined {
        // we need to remove other cards
        // originally the Suit is an enum but it's converted to a number
        // by typescript under the hood
        const flushCards = hand.suits.find( cardArr => cardArr.length >= 5);
        if (flushCards)
            return this.findHighests(amount, flushCards);
    }


    evaluateStraight(hand: Hand): Card[] | undefined {
        let consecutives: Card[] = [];
        const length = hand.ranks.length;
        // for A2345 we put the A already in the consecutive array
        if (hand.hasRank(Rank.ACE))
            consecutives.push(hand.ranks[Rank.ACE][0])

        // we loop through each rank in hand, if we find a group of card
        // we push the first one of the group into consecutives
        // if there is no card at said rank we reset consecutives.
        for (let i = 0; i < length; i++) {
            // we are only sure there is at least one card at that rank
            if (hand.hasRank(i))
                consecutives.push(hand.ranks[i][0]);
            else
                consecutives = [];
            // if we have 5 consecutives cards we still need to check
            // that there isn't anymore after
            if (consecutives.length >= 5) {
                const nextCards = hand.ranks[i + 1];
                if (nextCards && nextCards.length === 0) {
                    break;
                }
            }
        }
        if (consecutives.length >= 5)
            return consecutives.reverse().slice(0, 5);
    }

    evaluateFullHouse(hand: Hand): Card[] | undefined {
        const set = this.findHighestArr(3, hand);
        if (set){
            const pair = this.findHighestArr(2, hand, set[0]);
            if (pair)
                return [...set, ...pair];
        }
    }

    evaluateFourOfAKind(hand: Hand): Card[] | undefined {
        const four = hand.ranks.find(cardArr => cardArr.length === 4);
        if (four) {
            four.push(...this.findHighests(1, hand.cards, four));
            return four;
        }
    }

    evaluateSet(hand: Hand): Card[] | undefined {
        const set = this.findHighestArr(3, hand);
        if (set) {
            set.push(...this.findHighests(2, hand.cards, set));
            return set;
        }
    }

    evaluateDoublePair(hand: Hand): Card[] | undefined {
        const pair1 = this.findHighestArr(2, hand);
        let pair2;
        if (pair1)
            pair2 = this.findHighestArr(2, hand, pair1[0]);
        if (pair1 && pair2){
            const combination = [ ...pair1, ...pair2 ];
            return [...combination, ...this.findHighests(1, hand.cards, combination)]
        }
    }

    evaluatePair(hand: Hand): Card[] | undefined {
        const pair = this.findHighestArr(2, hand);
        if (pair) {
            pair.push(...this.findHighests(3, hand.cards, pair));
            return pair;
        }
    }

    findHighestArr(length: number, hand: Hand, omitted?: Card): Card[] | undefined {
        let ranksReverse = [...hand.ranks].reverse();
        // omit the ones we don't want by checking omitted rank and rank.firstcard.rank
        if (omitted)
            ranksReverse = ranksReverse.filter(arr => arr[0] && arr[0].rank !== omitted.rank);
        const set = ranksReverse
            .find(arr => arr.length >= length);
        if (set)
            return set.slice(0, length);
    }

    // get x highest number of cards
    findHighests(amount: number, cards: Card[] = [], omitted: Card[] = []): Card[] {
        // !~indexOf = not found
        const relevant =  (cards.filter(c => !~omitted.indexOf(c)) as Card[])
            .sort(Card.sortFn);
        return relevant.slice(0, amount);
    }


}

Winning Hand

// A hand consist of the cards the player is holding + the cards on board
// Which equals to 7 cards.
// A winning hand is the best possible combination of 5 cards from those 7 cards
export class WinningHand extends Hand{

    constructor(cards: Card[], public rank: HandRank) {
        super();
        super.cards = cards;
    }

    // If a hand has a rank of PAIR
    // we need to be able to compare it with another
    // wining hand that is also pair. Thus we need additional information,
    // like the high card, etc.
    // We will total 6 ranks
    calculateRanks(rank: HandRank){
        // TODO
    }
}

Also a few enums

export enum Suit {
    HEARTH,
    SPADE,
    DIAMOND,
    CLUB
}

export enum Rank {
    TWO,
    THREE,
    FOUR,
    FIVE,
    SIX,
    SEVEN,
    EIGHT,
    NINE,
    TEN,
    JACK,
    QUEEN,
    KING,
    ACE
}

export enum HandRank {
    HIGH_CARD,
    PAIR,
    DOUBLE_PAIR,
    SET,
    STRAIGHT,
    FLUSH,
    FULL_HOUSE,
    FOUR_OF_A_KIND,
    STRAIGHT_FLUSH,
    ROYAL_FLUSH,
}
\$\endgroup\$
6
  • \$\begingroup\$ I am not strong on java. Syntactically it looks OK but it is not very efficient. If you are just evaluating a table then fine but it would die if you used it for simulations. You don't need a separate ROYAL_FLUSH. What a player holds are called hole cards. Not seeing the purpose of static fromString(str: string). Pass the rank and suit. \$\endgroup\$
    – paparazzo
    Jan 15, 2018 at 17:29
  • \$\begingroup\$ @Paparazzi the point of fromString was to make unit testing easy. I just had to do tests like expect(handEvaluator.evaluateHand(Hand.fromString('7d 8s 9c Th Qd Js Ah')).rank).toBe(HandRank.STRAIGHT);. It has been really handy tbh. \$\endgroup\$
    – Ced
    Jan 15, 2018 at 21:15
  • \$\begingroup\$ You base design on making unit testing easy? \$\endgroup\$
    – paparazzo
    Jan 15, 2018 at 21:33
  • \$\begingroup\$ @Paparazzi it's a helper function. Readability / ease of use is indeed something I think is important. So yes \$\endgroup\$
    – Ced
    Jan 15, 2018 at 22:38
  • \$\begingroup\$ Ease of unit testing is your top priority? Let the user that does not know valid strings just figure. \$\endgroup\$
    – paparazzo
    Jan 16, 2018 at 0:06

3 Answers 3

4
+100
\$\begingroup\$

Object oriented programming

Sorry this is not a good review, your code is a classic example of a quote often cited in the argument against OOP.

"You wanted a banana but what you got was a gorilla holding the banana and the entire jungle." Joe Armstrong

I will handle this as script agnostic

Too many classes

The solution you have created is just too complex. the code seams fixated on irrelevant data, and does little to aid the recovery of the wanted data.

You are after the best hand out of 7 cards, for that you have created 3 separate classes. The class Card provides no useful additional information above what a simple string representation can hold, nor has it added any useful functionality that can not be done in a simple one line expression. "The jungle"

The Hand's class only function is to vet the input, it then stores the cards without adding any information. You have simply converted from one abstraction to alternative equal level abstraction. "A gorilla."

The HandEvaluator class is the only class that does anything of value, but you have created it to be instantiated rather than as a set of static functions. 'It creates the wanted banana'

The winningHand is the banana? or a gorilla? seams that the best hand is lost as no where can I see a way to find out what the best hand is. It got lost in the jungle.

More than one best hand is possible.

On top of that the resulting information is not strictly correct. Given a set of 7 cards there are 21 possible 5 card combinations. Of those 21 hands one or more hands can represent the best hand. Though it does not matter which one of the best hands is picked it seams that the best hand selection is arbitrary.

Also you do a lot of reprocessing, with each evaluation step repeating many of the same functions as the step before. Data should only be processed once. Also you are constantly creating new arrays evaluated cards.

Static functions.

This should be done as a two static functions that return a simple object representing the best hand as a score that can be compared to all other possible hands. Maybe the hand name and cards as a string would be of use as well. All the many other object instances of hands, cards,... are not needed at any point.

The rest of this answer is an alternative solution. It is not a perfect solution but just investigates the problem to a little more depth, and shows how you can solve the problem without unneeded overhead.

Looking at the problem.

Input string => vet string => evaluate all 5 card combinations => return array of equal best hands.

Vetting the input string.

There are 3 possible errors

  • Duplicated card
  • Malformed card
  • Incorrect card count.

During the vetting process you can also do some processing to the input data adding information and tokenise the cards to a more machine readable form.

In the following example the string is parsed, if there is an error an error message is return, if the cards are valid an array of card values is returned as a single value for each card. The card value (you call rank) is stored as the first 4 bits, and the suit is bits 5,6. The cards are also sorted from low to high to make further processing simpler.

// suit and rank string encodings
const suitNames = "hsdc";
const rankNames = "23456789TJQKA";
// this would be a property of a hand evaluater
parseString(input){       
    const cards = input.split(" ");
    var parsedCards;
    // Check card count
    if (cards.length !== 7) { return "Bad input card count not 7" }       
    // convert card string representation to encoded integer
    // 4 low bits is value (rank) and next two bits is suit
    // Using throw to exit the map iteration
    try {
        parsedCards = cards.map(card => {
            var rank = rankNames.indexOf(card[0]);
            if (rank === -1) { throw new Error(card) } // if rank not found must be an error
            var suit = suitNames.indexOf(card[1]);
            if (suit === -1) { throw new Error(card) } // if suit not found must be an error
            return  rank + (suit << 4);  // encode and add to array
        });
    } catch(e) { return "Bad input invalid card " + e.message }
    // Sort cards from low to high. This allows you to also add a duplication test
    // Using throw to exit the sort iteration
    try {
        parsedCards.sort((a, b) => {
            if (a === b) { throw new Error() }  // if to cards have the same value this must be a duplication
            const dif = (a & 15) - (b & 15); // get rank difference
            if (dif === 0) { return a - b }  // if same rank sort on suit
            return dif;
        });
    } catch(e) {  return "Bad input duplicated card "  }
    return parsedCards; // return the array of parse cards
},
// the following function converts an encoded card to a string
cardToString(card){ return rankNames[card & 15] + suitNames[card >> 4] },


// to use
var cards = handEvaluator.parseString("8s 9c Qh 7d Ac Th Qh");
if(typeof cards === "string"){
    // display error message
}else{
    // cards is an array of numbers.
}

Evaluate all 5 card combinations

There are 10 named hands in order of value from lowest to highest

const handNames = {
    HIGH_CARD       : 0,
    PAIR            : 1,
    DOUBLE_PAIR,    : 2,
    SET,            : 3,
    STRAIGHT,       : 4,
    FLUSH,          : 5,
    FULL_HOUSE,     : 6,
    FOUR_OF_A_KIND, : 7,
    STRAIGHT_FLUSH, : 8,
    ROYAL_FLUSH,    : 9,
}

All but the royal flush has an additional value depending on the highest card. If you multiply each named hand index by 13 (the number of card values "23456789TJQKA") you get a unique score for each hand thus a full house with high ace would have a score of 6 * 13 + 12 = 90 which would beat a full house king high 6 * 13 + 11 = 89 . This system would produce a gap between the best straight flush ace high and the royal flush that is always ace high. But that would not effect score comparison.

There are 21 possible 5 card combinations in a deck of 7. You could build a function that generates that set of 21 combinations, but as this problem is static (it will only ever deal with 7 cards) the combos can be precomputed and stored as a static list

// each charcter represents the index into the 7 card array
const allPerms = "01234,01235,01236,01245,01246,01256,01345,01346,01356,01456,02345,02346,02356,02456,03456,12345,12346,12356,12456,13456,23456".split(",");
// thus the  permutation 12346 represent the 2nd, 3rd, 4th, 5th and 7th card in the 7 card array

As the cards are now encoded in a 6bit int we can use a regExp to evaluate sets of cards by converting an encoded card to a hex value.

For example the card Ac (Ace = = 12 of clubs = 3) is encoded as 12 + 3 * 16 = 60 is the hex value 3c King of clubs 3b queen of hearts as 0a. A royal flush would have the hex representation of "38393a3b3c", "28292a2b2c", "18191a1b1c", "08090a0b0c"

But if we create two string. One that is the suits and the other the ranks we get [["33333","89abc"], ["22222","89abc"], ["11111","89abc"], ["00000","89abc"]]. That means a straigh is one of 9 squences "01234", "12345", "23456", "34567", "45678", "56789", "6789a", "789ab", "89abc" and a flush is one of suits "00000", "11111", "22222", "33333".

We can create very simple reg expressions to test for a particular type of hand.

// six reg expressions that can be used to find hands when 5 cards are split into ranks and suit in order
// from low to high.
const flushes       = /00000|11111|22222|33333/;
const straights     = /01234|12345|23456|34567|45678|56789|6789a|789ab|89abc/;
const fourOfKind    = /0000|1111|2222|3333|4444|5555|6666|7777|8888|9999|aaaa|bbbb|cccc/;
const threeOfKind   = /000|111|222|333|444|555|666|777|888|999|aaa|bbb|ccc/;        
const twoOfKind     = /00|11|22|33|44|55|66|77|88|99|aa|bb|cc/;        
const fullHouse     = /(000|111|222|333|444|555|666|777|888|999|aaa|bbb|ccc)(00|11|22|33|44|55|66|77|88|99|aa|bb|cc)|(00|11|22|33|44|55|66|77|88|99|aa|bb|cc)(000|111|222|333|444|555|666|777|888|999|aaa|bbb|ccc)/;
const twoPair       = /(00|11|22|33|44|55|66|77|88|99|aa|bb|cc).*(00|11|22|33|44|55|66|77|88|99|aa|bb|cc)/;

This is a very crude set of reg expressions, and I am sure there are better ones, I leave that to you if you think these approaches suit you.

Returning best hands

All that is then left to do is test each of the 21 hands and giving them a score. Once scored you sort them from best score down and filter keeping only hands that have the same score as the best.

You then get an array of best hands, each with a score that can be directly tested against any other hand. No need to have the high card. No need to have an object holding array of card objects, that is all irrelevant to the result.

Example

Putting it all together you can create a hand evaluater that will return all the equal best hands from the parsed array of 7 cards.

For fun (and to make sure it worked) I have added an interface. Click cards to create a deck, when 7 cards selected they are evaluated. Click on selected cards to remove. Evaluates and names all combos. Best hand has lowest score.

    const doFor = (count, callback) => { var i = 0; while (i < count && callback(i++) !== true ); }
    const suitNames = "HSDC";
    const rankNames = "23456789TJQKA";
    const allPerms = "01234,01235,01236,01245,01246,01256,01345,01346,01356,01456,02345,02346,02356,02456,03456,12345,12346,12356,12456,13456,23456".split(",");
    const cardToString = (card) => rankNames[card & 15] + suitNames[card >> 4];
    const rankingNames = "Royal flush,Straight flush,Four of a kind,Full house,Flush,Straight,Three of a kind,Two pair,Two of a kind,".split(",");
    const flushes       = /00000|11111|22222|33333/;
    const straights     = /01234|12345|23456|34567|45678|56789|6789a|789ab|89abc/;
    const fourOfKind    = /0000|1111|2222|3333|4444|5555|6666|7777|8888|9999|aaaa|bbbb|cccc/;
    const threeOfKind   = /000|111|222|333|444|555|666|777|888|999|aaa|bbb|ccc/;        
    const twoOfKind     = /00|11|22|33|44|55|66|77|88|99|aa|bb|cc/;        
    const fullHouse     = /(000|111|222|333|444|555|666|777|888|999|aaa|bbb|ccc)(00|11|22|33|44|55|66|77|88|99|aa|bb|cc)|(00|11|22|33|44|55|66|77|88|99|aa|bb|cc)(000|111|222|333|444|555|666|777|888|999|aaa|bbb|ccc)/;
    const twoPair       = /(00|11|22|33|44|55|66|77|88|99|aa|bb|cc).*(00|11|22|33|44|55|66|77|88|99|aa|bb|cc)/;

    // snippets have no support for modules but this code
    // should run as a module to encapsulate all that stuff above.
    //export default const handEvaluator = {
    
    const handEvaluator = {
        parseString(input){       
            const cards = input.split(" ");
            var parsedCards;
            if (cards.length !== 7) { return "Bad input card count not 7" }       
            try {
                parsedCards = cards.map(card => {
                    var rank = rankNames.indexOf(card[0]);
                    if (rank === -1) { throw new Error(card) }
                    var suit = suitNames.indexOf(card[1]);
                    if (suit === -1) { throw new Error(card) }
                    return  rank+ (suit << 4);
                });
            } catch(e) { return "Bad input invalid card " + e.message }
            try {
                parsedCards.sort((a, b) => {
                    if (a === b) { throw new Error() }
                    const dif = (a & 15) - (b & 15);
                    if (dif === 0) { return a - b }
                    return dif;
                });
            } catch(e) {  return "Bad input duplicated card "  }
            return parsedCards
        },
        evaluate(cardsArray){
            var suited, ranked, hand;  // holds the hex encoded 5 cards of suit only, rank only and the hand
                                       // in the original format
            var allHandsRanked = []; // to hold all combinations of 5 cards an the scores    
            const tests = {  // a set of named tests that test a hand and return true if the hand matches
                royalFlush    : () => tests.flush() && tests.straight() && tests.royal(),
                straightFlush : () => tests.flush() && tests.straight(),
                kind4         : () => fourOfKind.test(ranked),
                fullHouse     : () => fullHouse.test(ranked),
                flush         : () => flushes.test(suited),
                straight      : () => straights.test(ranked),
                kind3         : () => threeOfKind.test(ranked),
                twoPair       : () => twoPair.test(ranked),
                kind2         : () => twoOfKind.test(ranked),
                highCard      : () => true,  // always true last type checked
                royal         : () => ranked[4] === "c",  // extra test used for royal flush
            };
            const ranking = Object.values(tests); // above tests as a indexed array
            // gets index that represents one of the 21 permutations and sets
            // ranked, suited and hand to that combination
            function getPermutation(index) {
                ranked = suited = hand = "";
                doFor(5, (i) => {
                    const card = cardsArray[allPerms[index][i]];
                    ranked += (card & 15).toString(16);
                    suited += (card >> 4).toString(16);
                    hand += " " + cardToString(card);
                });
                hand = hand.substr(1);
            };
            // Rank the current hand with best score 0 
            function rankHand(permutation) {
                getPermutation(permutation); // get the permutation
                doFor(ranking.length, (i) => {  // test each type of hand from best to worst
                    if (ranking[i]()) {  // if test passed
                        allHandsRanked.push({ // add the hand and score the hand
                            name : rankingNames[i],
                            hand : hand,
                            score : i * 13 + (12-parseInt(ranked[4],16)),
                        });
                        return true; 
                    }
                });
            }
            doFor(allPerms.length, rankHand); // test all permutations
            // for demo I return all cards commented code below the 
            // return return only best cards
            return allHandsRanked
                .sort((a,b) => a.score - b.score);
            
            
            // sort and filter returning only the equal best hands
            /*
            return allHandsRanked
                .sort((a,b) => a.score - b.score)
                .filter((hand,i,arr)=> i=== 0 ? true : hand.score === arr[i-1].score); 
                */
        }
    };
    
    
    
    
    
/*====================================================================
   For fun I added an interface. The code from here down is a rush job
   (aka an uncommented mess) and should not be used as an example
=====================================================================*/

    
    const cards = [
      "🂲 🂳 🂴 🂵 🂶 🂷 🂸 🂹 🂺 🂻 🂽 🂾 🂱",
      "🂢 🂣 🂤 🂥 🂦 🂧 🂨 🂩 🂪 🂫 🂭 🂮 🂡",
      "🃂 🃃 🃄 🃅 🃆 🃇 🃈 🃉 🃊 🃋 🃍 🃎 🃁",
      "🃒 🃓 🃔 🃕 🃖 🃗 🃘 🃙 🃚 🃛 🃝 🃞 🃑",
    ];
    const suits = "HSDC";
    const values = "23456789TJQKA";
    function getCard(str){
      var value = values.indexOf(str[0]);
      var suit = suits.indexOf(str[1]);
      
       var span = document.createElement("span");
       span.className = "res suit" + suits[suit]; 
       span.textContent = cards[suit].split(" ")[value];
      return span;

    }
    var addTo;
    cards.forEach((suit,i) => {
        cardContainor.appendChild(addTo = document.createElement("div"));
        suit.split(" ").forEach((card,j) => {
        
          addButton(card,suits[i],values[j]);
          
        });
    })
    addTo = selectedCards;

    function addButton(text,suit,value){
       var span = document.createElement("span");
       span.className = "button suit" + suit; 
       span.textContent = text;
       span.suit = suit;
       span.cvalue = value;
       addTo.appendChild(span);
       return span;
    }

    var selectedCount = 0;
    cardContainor.addEventListener("click",(e) => {
       if(e.target.suit === undefined){ return }
       if(selectedCount < 7){
           addButton(e.target.textContent,e.target.suit,e.target.cvalue);
           selectedCount += 1;
           if(selectedCount === 7){
               var cards = [...selectedCards.querySelectorAll(".button")];
               var str = [];
               
               
               cards.forEach(card => {

                str.push(card.cvalue + card.suit );
               });
              var  vettedInput = handEvaluator.parseString(str.join(" "));
              if(typeof vettedInput === "string"){
                  report.textContent = vettedInput;
              }else{
                  report.innerHTML = "";
                  var results = handEvaluator.evaluate(vettedInput);
                  results.forEach(hand => {
                      var div = document.createElement("div");
                      div.className = "results";
                      
                      var span = document.createElement("span");
                      hand.hand.split(" ").forEach(card => {
                          span.appendChild(getCard(card));
                       })
                       div.appendChild(span);
                       span = document.createElement("span");
                       span.textContent = " "+ hand.name + " : Score : " + hand.score + " High card : ";
                       
                       div.appendChild(span);
                       div.appendChild(getCard(hand.hand.substr(12))) 

                       report.appendChild(div);
                      
                  
                  })

              }           
           }
       }
    })
    selectedCards.addEventListener("click",(e) => {
       if(e.target.suit !== undefined){
         selectedCards.removeChild(e.target);
         selectedCount -= 1;
       }
    })
    body {
       font-family : arial;
    }
    .button {
        font-size : 45px;
        margin-left: -0px;
        margin-right: -0px;
        margin-top: -3px;
        cursor : pointer;
    }
    .button:hover {
       background : #AFA;
    }
    .suitH {
    color : red;
    }
    .suitD {
    color : red;
    }
    .suitC {
    color : black;
    }
    .suitS {
    color : black;
    }

    div {
    margin-top: -16px;
    }
    .results {
    margin-top: 0px;

    }
    .res {
    font-size : 32px;
    }


 
    <div id="cardContainor"></div>

    <div id="selectedCards"></div>
    <br>
    <br>

    <div id="report"></div>

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the detailed explanation, really cool! However, the demo from the code snippet is incorrectly calculating the score for the hands. It is actually ranking the best hand the smallest score with the weakest hand having the best score. Just thought I’d pointed that out. Thanks again! \$\endgroup\$
    – alchuang
    Apr 19, 2020 at 21:26
2
\$\begingroup\$

I think your suits and ranks should be String union types. That way you wouldn't need the constructor to check if a card's rank and suit is valid - the compiler would do it for you.

As for your HandEvaluator, I think it is messy due to repeatedly asking for things you already know and this whole winningcards = functioncall() inside an if.

I don't like how you, for purposes of determining a royal flush, first check for a straight flush, then throw the result away if it doesn't match, only to then recalculate the straight flush on the next function call.

It seems like it would be better to split hand evaluation into a couple categories: The highest rank, the highest x-of-a-kind, the highest pairs (including full house) and the best straight/flush.

Start with the straight/flush - if you get a straight flush or royal flush you're done. Otherwise, evaluate the highest x-of-a-kind - if it's 4 of a kind, you're done. Else, check if the pairs evaluation comes up with full house - then you would be done on that front. Otherwise, check the x-of-a-kind result for a 3 of a kind, else check the pairs for a double pair or single pair, and if that doesn't work, straight up return the result of the highest rank.

Like this, you evaluate each category once, and then you just start "sorting" the results.


The evaluateHand function in Hand doesn't seem to actually evaluate the hand, what is its purpose? Since you have a HandEvaluator, does a Hand still need an evaluateHand function?

\$\endgroup\$
0
\$\begingroup\$

A clearer way to evaluate a hand is to make a pair of histograms, one of rank and one of suits. For the rank, if one of the bins is 4 you've got 4 of a kind. If one is 3 and another 2 you've got a boat, etc etc. If one of the suit bins is 5 you've got a flush. If high card - low card == 4 you've got a straight (aces make this interesting). etc etc etc.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.