3
\$\begingroup\$

Given a two-card poker hand and a board of 5 cards, I want to evaluate the player's hand strength as listed here.

The evaluation of a poker hand was measured to be ~100 microseconds on average. Existing free equity calculators are able to perform this calculation in under 1 microsecond (for example, Equilab). I would like to know how to increase the performance of my algorithm to that level.

The function that evaluates the hand is unique_ptr<ShowdownHand> eval_hand(const PokerHand& hand, const Board& board) in showdown.h.

Here is the complete code:

poker_game.h

#pragma once

#include <string>
#include <vector>
#include <memory>


namespace Poker {
    // CardSuit

    enum class CardSuit : char {
        CLUB = 'c',
        HEART = 'h',
        SPADE = 's',
        DIAMOND = 'd',
        PLACEHOLDER = 'x'
    };


    // CardRank

    class CardRank {
    public:
        const static CardRank C_2;
        const static CardRank C_3;
        const static CardRank C_4;
        const static CardRank C_5;
        const static CardRank C_6;
        const static CardRank C_7;
        const static CardRank C_8;
        const static CardRank C_9;
        const static CardRank C_T;
        const static CardRank C_J;
        const static CardRank C_Q;
        const static CardRank C_K;
        const static CardRank C_A;
        const static CardRank PLACEHOLDER;

        static CardRank from_val(int val);
        static CardRank from_repr(char repr);

        int get_val() const { return val; };
        char get_repr() const { return repr; };

        const CardRank& operator++();
        const CardRank& operator--();

    private:
        constexpr CardRank(int val, char repr) : val{ val }, repr{ repr } {};

        int val;
        char repr;
    };

    inline bool operator<(CardRank hero, CardRank vill) { return hero.get_val() < vill.get_val(); }
    inline bool operator>(CardRank hero, CardRank vill) { return vill < hero; }
    inline bool operator==(CardRank hero, CardRank vill) { return !(hero < vill) && !(hero > vill); }
    inline bool operator!=(CardRank hero, CardRank vill) { return !(hero == vill); }
    inline bool operator>=(CardRank hero, CardRank vill) { return hero > vill || hero == vill; }
    inline bool operator<=(CardRank hero, CardRank vill) { return hero < vill || hero == vill; }

    // Card

    class Card {
    public:
        Card() : rank{ CardRank::PLACEHOLDER }, suit{ CardSuit::PLACEHOLDER } {}
        Card(CardRank rank, CardSuit suit) : rank{ rank }, suit{ suit } {}
        Card(std::string card_str);

        CardRank get_rank() const { return rank; }
        CardSuit get_suit() const { return suit; }
        std::string repr() const { return std::string{} + rank.get_repr() + static_cast<char>(suit); }

        const Card& operator++() { ++rank; }
        const Card& operator--() { --rank; }

    private:
        CardRank rank;
        CardSuit suit;
    };

    inline bool operator<(Card hero, Card vill) { return hero.get_rank() < vill.get_rank(); }
    inline bool operator>(Card hero, Card vill) { return vill < hero; }
    inline bool operator==(Card hero, Card vill) { return !(hero < vill) && !(hero > vill); }
    inline bool operator!=(Card hero, Card vill) { return !(hero == vill); }
    inline bool operator>=(Card hero, Card vill) { return hero > vill || hero == vill; }
    inline bool operator<=(Card hero, Card vill) { return hero < vill || hero == vill; }


    // PokerHand

    class PokerHand {
    public:
        PokerHand(Card card1, Card card2);
        PokerHand(CardRank rank1, CardSuit suit1, CardRank rank2, CardSuit suit2);
        PokerHand(std::string hand_str);

        const Card& get_primary() const { return primary; }
        const Card& get_secondary() const { return secondary; }
        bool is_suited() const { return primary.get_suit() == secondary.get_suit(); }
        bool is_pp() const { return primary.get_rank() == secondary.get_rank(); }
        std::string repr() const { return primary.repr() + secondary.repr(); }

    private:
        Card primary;
        Card secondary;
    };


    // Street

    enum class Street {
        PREFLOP = 0, FLOP = 3, TURN = 4, RIVER = 5
    };


    // Board

    class Board {
    public:
        Board() { cards.reserve(5); }
        Board(const std::vector<Card>& cards) : cards{ cards } {}
        Board(const std::vector<Card>&& cards) : cards{ cards } {}
        Board(std::string board_str);

        void add_card(const Card& card) { cards.push_back(card); }
        void pop_card() { cards.pop_back(); }
        Street street() const { return static_cast<Street>(cards.size()); }
        int count(CardRank rank) const;
        int count(CardSuit rank) const;

        const Card& operator[](int i) const { return cards[i]; }
        Card& operator[](int i) { return cards[i]; }

        std::vector<Card>::iterator begin() { return cards.begin(); }
        std::vector<Card>::iterator end() { return cards.end(); }
        std::vector<Card>::const_iterator begin() const { return cards.cbegin(); }
        std::vector<Card>::const_iterator end() const { return cards.cend(); }
        std::vector<Card>::reverse_iterator rbegin() { return cards.rbegin(); }
        std::vector<Card>::reverse_iterator rend() { return cards.rend(); }

    private:
        std::vector<Card> cards;
    };
}

poker_game.cpp

#include "poker_game.h"
#include "random.h"

#include <stdexcept>
#include <list>
#include <iostream>
#include <algorithm>
#include <functional>

using std::cout;
using std::endl;

namespace Poker {
    // CardRank

    const CardRank CardRank::C_2{ 0, '2' };
    const CardRank CardRank::C_3{ 1, '3' };
    const CardRank CardRank::C_4{ 2, '4' };
    const CardRank CardRank::C_5{ 3, '5' };
    const CardRank CardRank::C_6{ 4, '6' };
    const CardRank CardRank::C_7{ 5, '7' };
    const CardRank CardRank::C_8{ 6, '8' };
    const CardRank CardRank::C_9{ 7, '9' };
    const CardRank CardRank::C_T{ 8, 'T' };
    const CardRank CardRank::C_J{ 9, 'J' };
    const CardRank CardRank::C_Q{ 10, 'Q' };
    const CardRank CardRank::C_K{ 11, 'K' };
    const CardRank CardRank::C_A{ 12, 'A' };
    const CardRank CardRank::PLACEHOLDER{ -1, 'X' };

    CardRank CardRank::from_val(int val) 
    {
        // TODO: Add stacktrace to exception

        switch (val) {
        case 0: return CardRank::C_2; break;
        case 1: return CardRank::C_3; break;
        case 2: return CardRank::C_4; break;
        case 3: return CardRank::C_5; break;
        case 4: return CardRank::C_6; break;
        case 5: return CardRank::C_7; break;
        case 6: return CardRank::C_8; break;
        case 7: return CardRank::C_9; break;
        case 8: return CardRank::C_T; break;
        case 9: return CardRank::C_J; break;
        case 10: return CardRank::C_Q; break;
        case 11: return CardRank::C_K; break;
        case 12: return CardRank::C_A; break;
        default: throw std::invalid_argument("CardRank::from_val - Invalid Argument: val =" + val);
        }
    }

    CardRank CardRank::from_repr(char repr) 
    {
        // TODO: Add stacktrace to exception

        switch (repr) {
        case '2': return CardRank::C_2; break;
        case '3': return CardRank::C_3; break;
        case '4': return CardRank::C_4; break;
        case '5': return CardRank::C_5; break;
        case '6': return CardRank::C_6; break;
        case '7': return CardRank::C_7; break;
        case '8': return CardRank::C_8; break;
        case '9': return CardRank::C_9; break;
        case 'T': return CardRank::C_T; break;
        case 'J': return CardRank::C_J; break;
        case 'Q': return CardRank::C_Q; break;
        case 'K': return CardRank::C_K; break;
        case 'A': return CardRank::C_A; break;
        default: throw std::invalid_argument("CardRank::from_repr - Invalid Argument: repr = " + repr);
        }
    }
    
    const CardRank& CardRank::operator++() 
    {
        ++val;
        if (val == 13) val = 0;
        repr = from_val(val).repr;

        return *this;
    }

    const CardRank& CardRank::operator--()
    {
        --val;
        if (val == -1) val = 12;
        repr = from_val(val).repr;

        return *this;
    }


    // Card

    Card::Card(std::string card_str) : 
        rank{ CardRank::from_repr(card_str[0]) },
        suit{ static_cast<CardSuit>(card_str[1]) } {}


    // PokerHand

    PokerHand::PokerHand(Card card1, Card card2) : primary{ card1 }, secondary{ card2 }
    {
        if (card1 < card2) {
            primary = card2;
            secondary = card1;
        }
    }

    PokerHand::PokerHand(CardRank rank1, CardSuit suit1, CardRank rank2, CardSuit suit2) :
        PokerHand(Card(rank1, suit1), Card(rank2, suit2)) {}

    PokerHand::PokerHand(std::string hand_str) : 
        PokerHand(Card(hand_str.substr(0, 2)), Card(hand_str.substr(2, 2))) {}


    // Board

    Board::Board(std::string board_str) 
    {
        cards.reserve(5);

        for (unsigned int i = 0; i < board_str.size(); i += 2) {
            cards.push_back(Card(board_str.substr(i, 2)));
        }
    }

    int Board::count(CardRank rank) const 
    {
        int count = 0;
        for (const Card& c : cards) {
            if (c.get_rank() == rank) {
                count++;
            }
        }

        return count;
    }

    int Board::count(CardSuit suit) const 
    {
        int count = 0;
        for (const Card& c : cards) {
            if (c.get_suit() == suit) {
                count++;
            }
        }

        return count;
    }
}

showdown.h

#pragma once

#include "poker_game.h"

#include <string>
#include <vector>
#include <memory>


namespace Poker {
    // HandType

    enum class HandType : char {
        HIGH_CARD = 0,
        PAIR = 1,
        TWO_PAIR = 2,
        TRIPS = 3,
        STRAIGHT = 4,
        FLUSH = 5,
        FULL_HOUSE = 6,
        QUADS = 7,
        STRAIGHT_FLUSH = 8,
        ROYAL_FLUSH = 9
    };

    inline bool operator<(HandType hero, HandType vill) { return static_cast<int>(hero) < static_cast<int>(vill); }
    inline bool operator>(HandType hero, HandType vill) { return vill < hero; }
    inline bool operator==(HandType hero, HandType vill) { return !(hero < vill) && !(hero > vill); }
    inline bool operator!=(HandType hero, HandType vill) { return !(hero == vill); }
    inline bool operator>=(HandType hero, HandType vill) { return hero > vill || hero == vill; }
    inline bool operator<=(HandType hero, HandType vill) { return hero < vill || hero == vill; }


    // ShowdownHand

    enum class Winner {
        HERO, VILL, SPLIT
    };

    class ShowdownHand {
    public:
        virtual ~ShowdownHand() = default;

        HandType get_hand_type() const { return hand_type; }
        Winner get_winner(const ShowdownHand& vill) const;
        std::string repr() const;

    protected:
        ShowdownHand(HandType hand_type, std::vector<Card> cards)
            : hand_type{ hand_type }, cards{ cards } {}

        const std::vector<Card>& get_cards() const { return cards; }
        const std::vector<Card>& get_best_hand() const { return best_hand; }
        void set_best_hand() const { best_hand = calc_best_hand(); }

    private:
        virtual Winner v_get_winner(const ShowdownHand& vill) const = 0;
        virtual std::vector<Card> calc_best_hand() const = 0;

        const HandType hand_type;
        const std::vector<Card> cards;
        mutable std::vector<Card> best_hand;
    };

    std::unique_ptr<ShowdownHand> eval_hand(const PokerHand& hand, const Board& board);


    // HighCard : ShowdownHand

    class HighCard : public ShowdownHand {
    public:
        HighCard(const std::vector<Card> cards) : ShowdownHand(HandType::HIGH_CARD, cards) {}

    private:
        Winner v_get_winner(const ShowdownHand& inp_vill) const override;
        std::vector<Card> calc_best_hand() const override;
    };


    // Pair : ShowdownHand

    class Pair : public ShowdownHand {
    public:
        Pair(CardRank pair_rank, const std::vector<Card> cards)
            : ShowdownHand(HandType::PAIR, cards), pair_rank{ pair_rank } {}

    private:
        Winner v_get_winner(const ShowdownHand& inp_vill) const override;
        std::vector<Card> calc_best_hand() const override;
        void cache_kickers() const;

        const CardRank pair_rank;
        mutable std::vector<Card> kickers;
    };


    // TwoPair : ShowdownHand

    class TwoPair : public ShowdownHand {
    public:
        TwoPair(CardRank top_rank, CardRank bot_rank, const std::vector<Card> cards)
            : ShowdownHand(HandType::TWO_PAIR, cards), top_rank{ top_rank }, bot_rank{ bot_rank } {}

    private:
        Winner v_get_winner(const ShowdownHand& inp_vill) const override;
        std::vector<Card> calc_best_hand() const override;
        void cache_kicker() const;

        const CardRank top_rank;
        const CardRank bot_rank;
        mutable Card kicker;
    };


    // Trips : ShowdownHand

    class Trips : public ShowdownHand {
    public:
        Trips(CardRank set_rank, const std::vector<Card> cards)
            : ShowdownHand(HandType::TRIPS, cards), trips_rank{ set_rank } {}

    private:
        Winner v_get_winner(const ShowdownHand& inp_vill) const override;
        std::vector<Card> calc_best_hand() const override;
        void cache_kickers() const;

        const CardRank trips_rank;
        mutable std::vector<Card> kickers;
    };


    // Straight : ShowdownHand

    class Straight : public ShowdownHand {
    public:
        Straight(CardRank top_rank, const std::vector<Card> cards)
            : ShowdownHand(HandType::STRAIGHT, cards), top_rank{ top_rank } {}

    private:
        Winner v_get_winner(const ShowdownHand& inp_vill) const override;
        std::vector<Card> calc_best_hand() const override;

        const CardRank top_rank;
    };


    // Flush : ShowdownHand

    class Flush : public ShowdownHand {
    public:
        Flush(CardSuit flush_suit, const std::vector<Card> cards)
            : ShowdownHand(HandType::FLUSH, cards), flush_suit{ flush_suit } {}

    private:
        Winner v_get_winner(const ShowdownHand& inp_vill) const override;
        std::vector<Card> calc_best_hand() const override;

        const CardSuit flush_suit;
    };


    // FullHouse : ShowdownHand

    class FullHouse : public ShowdownHand {
    public:
        FullHouse(CardRank trips_rank, CardRank pair_rank, const std::vector<Card> cards)
            : ShowdownHand(HandType::FULL_HOUSE, cards), trips_rank{ trips_rank }, pair_rank{ pair_rank } {}

    private:
        Winner v_get_winner(const ShowdownHand& inp_vill) const override;
        std::vector<Card> calc_best_hand() const override;

        const CardRank trips_rank;
        const CardRank pair_rank;
    };


    // Quads : ShowdownHand

    class Quads : public ShowdownHand {
    public:
        Quads(CardRank quads_rank, const std::vector<Card> cards)
            : ShowdownHand(HandType::QUADS, cards), quads_rank{ quads_rank } {}

    private:
        Winner v_get_winner(const ShowdownHand& inp_vill) const override;
        std::vector<Card> calc_best_hand() const override;
        void cache_kicker() const;

        const CardRank quads_rank;
        mutable Card kicker;
    };


    // StraightFlush : ShowdownHand

    class StraightFlush : public ShowdownHand {
    public:
        StraightFlush(CardRank top_rank, CardSuit flush_suit, const std::vector<Card> cards)
            : ShowdownHand(HandType::STRAIGHT_FLUSH, cards), top_rank{ top_rank }, flush_suit{ flush_suit } {}

    private:
        Winner v_get_winner(const ShowdownHand& inp_vill) const override;
        std::vector<Card> calc_best_hand() const override;

        const CardRank top_rank;
        const CardSuit flush_suit;
    };

    class RoyalFlush : public ShowdownHand {
    public:
        RoyalFlush(CardSuit flush_suit, const std::vector<Card> cards)
            : ShowdownHand(HandType::ROYAL_FLUSH, cards), flush_suit{ flush_suit } {}

    private:
        Winner v_get_winner(const ShowdownHand& inp_vill) const override;
        std::vector<Card> calc_best_hand() const override;

        const CardSuit flush_suit;
    };
    Winner showdown(const PokerHand& hero, const PokerHand& vill, const Board& board);
}

showdown.cpp

#include "showdown.h"

#include <stdexcept>
#include <list>
#include <iostream>
#include <algorithm>
#include <functional>
#include <map>

namespace Poker {

    std::vector<Card> get_all_cards(const PokerHand& hand, const Board& board)
    {
        std::vector<Card> all_cards;

        all_cards.reserve(7);
        all_cards.push_back(hand.get_primary());
        all_cards.push_back(hand.get_secondary());
        all_cards.insert(all_cards.end(), board.begin(), board.end());

        return all_cards;
    }

    std::vector<Card> filter_cards(
        const std::vector<Card>& cards, const std::function <bool(const Card&)> filter)
    {
        std::vector<Card> filtered;
        filtered.reserve(7);

        for (const Card& card : cards) {
            if (filter(card)) {
                filtered.push_back(card);
            }
        }

        return filtered;
    }


    // eval_hand

    CardSuit check_flush(const std::map<CardSuit, int>& suits)
    {
        for (std::pair<CardSuit, int> e : suits) {
            if (e.second >= 5) {
                return e.first;
            }
        }

        return CardSuit::PLACEHOLDER;
    }

    CardRank check_straight(const std::vector<Card>& all_cards)
    {
        int counter = 0;
        int next_val;
        CardRank top_rank = CardRank::PLACEHOLDER;

        for (unsigned int i = 0; i < all_cards.size() - 1; i++) {
            if (all_cards[i].get_rank() == all_cards[i + 1].get_rank()) {
                continue;
            }

            next_val = all_cards[i].get_rank().get_val() - 1;

            if (all_cards[i + 1].get_rank().get_val() == next_val) {
                ++counter;
                if (counter == 1) top_rank = all_cards[i].get_rank();
                else if (counter == 4) return top_rank;
            }
            else {
                counter = 0;
            }
        }

        if (counter == 3 &&
            all_cards.back().get_rank() == CardRank::C_2 &&
            all_cards.front().get_rank() == CardRank::C_A)
        {
            return CardRank::C_5;
        }

        return CardRank::PLACEHOLDER;
    }

    void process_matches(const std::map<CardRank, int>& rank_matches,
        std::vector<CardRank>& pairs, std::vector<CardRank>& trips, CardRank& quads)
    {
        for (std::pair<CardRank, int> match : rank_matches) {
            if (match.second == 2) pairs.push_back(match.first);
            else if (match.second == 3) trips.push_back(match.first);
            else if (match.second == 4) quads = match.first;
        }
    }

    ShowdownHand* make_royal_flush(CardSuit flush_suit, const std::vector<Card>& all_cards)
    {
        int suit_counter = 0;

        for (unsigned int i = 0; i < all_cards.size() - 1; i++) {
            if (all_cards[i].get_rank() == all_cards[i + 1].get_rank()) {
                continue;
            }

            if (all_cards[i].get_suit() == flush_suit) {
                ++suit_counter;
                if (suit_counter == 5) {
                    break;
                }
            }
            else {
                return nullptr;
            }
        }

        return new RoyalFlush(flush_suit, all_cards);
    }

    ShowdownHand* make_straight_flush(CardRank top_rank, CardSuit flush_suit, const std::vector<Card>& all_cards)
    {
        int suit_counter = 0;
        int card_count = all_cards.size();

        for (int i = 0; i < card_count; i++) {
            if (all_cards[i].get_rank() > top_rank) {
                continue;
            }
            if (i != card_count - 1 &&
                all_cards[i].get_rank() == all_cards[i + 1].get_rank() &&
                all_cards[i].get_suit() != flush_suit)
            {
                continue;
            }

            if (i != 0 && 
                all_cards[i - 1].get_rank() == all_cards[i].get_rank() && 
                all_cards[i].get_suit() != flush_suit) 
            {
                continue;
            }
            if (suit_counter == 0 && all_cards[i].get_suit() != flush_suit) {
                --top_rank;
                continue;
            }
            

            if (all_cards[i].get_suit() == flush_suit) {
                ++suit_counter;
                if (suit_counter == 5) {
                    break;
                }
            }
            else {
                suit_counter = 0;
            }
        }

        return new StraightFlush(top_rank, flush_suit, all_cards);
    }

    std::unique_ptr<ShowdownHand> eval_hand(const PokerHand& hand, const Board& board)
    {
        std::vector<Card> all_cards = get_all_cards(hand, board);
        std::sort(all_cards.rbegin(), all_cards.rend());

        std::map<CardRank, int> rank_matches;
        std::map<CardSuit, int> suits;
        for (const Card& card : all_cards) {
            ++rank_matches[card.get_rank()];
            ++suits[card.get_suit()];
        }

        CardSuit flush_suit = check_flush(suits);
        CardRank straight_rank = check_straight(all_cards);
        bool is_flush = flush_suit != CardSuit::PLACEHOLDER;
        bool is_straight = straight_rank != CardRank::PLACEHOLDER;

        ShowdownHand* hand_ptr = nullptr;

        // Royal Flush, Straight Flush
        if (is_flush && is_straight) {
            if (straight_rank == CardRank::C_A) {
                hand_ptr = make_royal_flush(flush_suit, all_cards);
                if (hand_ptr) return std::unique_ptr<ShowdownHand>(hand_ptr);
            }
            else {
                hand_ptr = make_straight_flush(straight_rank, flush_suit, all_cards);
                if (hand_ptr) return std::unique_ptr<ShowdownHand>(hand_ptr);
            }
        }

        std::vector<CardRank> pairs;
        std::vector<CardRank> trips;
        CardRank quads = CardRank::PLACEHOLDER;
        process_matches(rank_matches, pairs, trips, quads);
        bool is_trips = trips.size() >= 1;
        bool is_pair = pairs.size() >= 1;
        if(is_pair) std::sort(pairs.rbegin(), pairs.rend());
        if(is_trips) std::sort(trips.rbegin(), trips.rend());
        

        // Quads
        if (quads != CardRank::PLACEHOLDER) {
            return std::unique_ptr<ShowdownHand>(new Quads(quads, all_cards));
        }

        // Full house
        if (is_trips) {
            if (is_pair) {
                return std::unique_ptr<ShowdownHand>(new FullHouse(trips[0], pairs[0], all_cards));
            }
            else if (trips.size() == 2) {
                return std::unique_ptr<ShowdownHand>(new FullHouse(trips[0], trips[1], all_cards));
            }
        }

        // Flush
        if (is_flush) {
            return std::unique_ptr<ShowdownHand>(new Flush(flush_suit, all_cards));
        }

        // Straight
        if (is_straight) {
            return std::unique_ptr<ShowdownHand>(new Straight(straight_rank, all_cards));
        }

        // Trips
        if (is_trips) {
            return std::unique_ptr<ShowdownHand>(new Trips(trips[0], all_cards));
        }

        // Two Pair
        if (pairs.size() >= 2) {
            return std::unique_ptr<ShowdownHand>(new TwoPair(pairs[0], pairs[1], all_cards));
        }

        // Pair
        if (is_pair) {
            return std::unique_ptr<ShowdownHand>(new Pair(pairs[0], all_cards));
        }

        // High Card
        return std::unique_ptr<ShowdownHand>(new HighCard(all_cards));
    }


    // ShowdownHand

    std::string hand_type_to_str(HandType hand_type)
    {
        switch (hand_type) {
        case HandType::HIGH_CARD: return "High Card";
        case HandType::PAIR: return "Pair";
        case HandType::TWO_PAIR: return "Two Pair";
        case HandType::TRIPS:   return "Trips";
        case HandType::STRAIGHT: return "Straight";
        case HandType::FLUSH: return "Flush";
        case HandType::FULL_HOUSE: return "Full House";
        case HandType::QUADS: return "Quads";
        case HandType::STRAIGHT_FLUSH: return "Straight Flush";
        case HandType::ROYAL_FLUSH: return "Royal Flush";
        default: throw std::invalid_argument("hand_type_to_str - Invalid Argument!");
        }
    }

    Winner ShowdownHand::get_winner(const ShowdownHand& vill) const
    {
        if (hand_type != vill.hand_type) {
            return hand_type > vill.hand_type ? Winner::HERO : Winner::VILL;
        }

        return v_get_winner(vill);
    }

    std::string ShowdownHand::repr() const
    {
        if (best_hand.size() != 5) {
            set_best_hand();
        }

        std::string hand_repr = hand_type_to_str(hand_type);
        hand_repr += " [";
        for (const Card& card : best_hand) {
            hand_repr += card.repr() + " ";
        }
        hand_repr.pop_back();
        hand_repr += "]";

        return hand_repr;
    }

    Winner compare_kickers(const std::vector<Card>& kickers_h, const std::vector<Card>& kickers_v) {
        for (unsigned int i = 0; i < kickers_h.size(); i++) {
            if (kickers_h[i] > kickers_v[i]) {
                return Winner::HERO;
            }
            else if (kickers_h[i] < kickers_v[i]) {
                return Winner::VILL;
            }
        }

        return Winner::SPLIT;
    }


    // HighCard : ShowdownHand

    Winner HighCard::v_get_winner(const ShowdownHand& inp_vill) const
    {
        const HighCard& vill = static_cast<const HighCard&>(inp_vill);
        const std::vector<Card>& kickers_h = get_best_hand();
        const std::vector<Card>& kickers_v = vill.get_best_hand();

        if (kickers_h.size() != 5) {
            set_best_hand();
        }

        if (kickers_v.size() != 5) {
            vill.set_best_hand();
        }

        return compare_kickers(kickers_h, kickers_v);
    }

    std::vector<Card> HighCard::calc_best_hand() const
    {
        const std::vector<Card>& cards = get_cards();
        std::vector<Card> best_hand(cards.begin(), cards.begin() + 5);

        return best_hand;
    }


    // Pair : ShowdownHand

    Winner Pair::v_get_winner(const ShowdownHand& inp_vill) const
    {
        const Pair& vill = static_cast<const Pair&>(inp_vill);

        if (pair_rank > vill.pair_rank) return Winner::HERO;
        if (pair_rank < vill.pair_rank) return Winner::VILL;

        if (kickers.size() != 3) cache_kickers();
        if (vill.kickers.size() != 3) vill.cache_kickers();

        return compare_kickers(kickers, vill.kickers);
    }

    std::vector<Card> Pair::calc_best_hand() const
    {
        if (kickers.size() != 3) {
            cache_kickers();
        }

        std::vector<Card> best_hand;

        best_hand.reserve(5);
        for (const Card& card : get_cards()) {
            if (card.get_rank() == pair_rank) {
                best_hand.push_back(card);
            }
        }
        best_hand.insert(best_hand.end(), kickers.begin(), kickers.end());

        return best_hand;
    }

    void Pair::cache_kickers() const
    {
        kickers = filter_cards(get_cards(), [this](const Card& card) { return card.get_rank() != pair_rank; });
        std::sort(kickers.rbegin(), kickers.rend());
        kickers.erase(kickers.begin() + 3, kickers.end());
    }


    // TwoPair : ShowdownHand

    Winner TwoPair::v_get_winner(const ShowdownHand& inp_vill) const
    {
        const TwoPair& vill = static_cast<const TwoPair&>(inp_vill);

        if (top_rank > vill.top_rank) return Winner::HERO;
        if (top_rank < vill.top_rank) return Winner::VILL;

        if (bot_rank > vill.bot_rank) return Winner::HERO;
        if (bot_rank < vill.bot_rank) return Winner::VILL;

        if (kicker.get_rank() == CardRank::PLACEHOLDER) cache_kicker();
        if (vill.kicker.get_rank() == CardRank::PLACEHOLDER) vill.cache_kicker();

        if (kicker > vill.kicker) return Winner::HERO;
        if (kicker < vill.kicker) return Winner::VILL;

        return Winner::SPLIT;
    }

    std::vector<Card> TwoPair::calc_best_hand() const
    {
        if (kicker.get_rank() == CardRank::PLACEHOLDER) {
            cache_kicker();
        }

        const std::vector<Card>& cards = get_cards();
        std::vector<Card> best_hand;

        best_hand.reserve(5);
        for (const Card& card : cards) {
            if (card.get_rank() == top_rank) {
                best_hand.push_back(card);
            }
        }
        for (const Card& card : cards) {
            if (card.get_rank() == bot_rank) {
                best_hand.push_back(card);
            }
        }
        best_hand.push_back(kicker);

        return best_hand;
    }

    void TwoPair::cache_kicker() const
    {
        std::vector<Card> cards = filter_cards(
            get_cards(),
            [this](const Card& card) {
            CardRank rank = card.get_rank();
            return rank != top_rank && rank != bot_rank;
        });

        kicker = *std::max_element(cards.begin(), cards.end());
    }


    // Trips : ShowdownHand

    Winner Trips::v_get_winner(const ShowdownHand& inp_vill) const
    {
        const Trips& vill = static_cast<const Trips&>(inp_vill);

        if (trips_rank > vill.trips_rank) return Winner::HERO;
        if (trips_rank < vill.trips_rank) return Winner::VILL;

        if (kickers.size() != 2) cache_kickers();
        if (vill.kickers.size() != 2) vill.cache_kickers();

        return compare_kickers(kickers, vill.kickers);
    }

    std::vector<Card> Trips::calc_best_hand() const
    {
        if (kickers.size() != 2) {
            cache_kickers();
        }

        std::vector<Card> best_hand;
        best_hand.reserve(5);

        for (const Card& card : get_cards()) {
            if (card.get_rank() == trips_rank) {
                best_hand.push_back(card);
            }
        }
        best_hand.insert(best_hand.end(), kickers.begin(), kickers.end());

        return best_hand;
    }

    void Trips::cache_kickers() const
    {
        kickers = filter_cards(get_cards(), [this](const Card& card) { return card.get_rank() != trips_rank; });
        std::sort(kickers.rbegin(), kickers.rend());
        kickers.erase(kickers.begin() + 2, kickers.end());
    }


    // Straight : ShowdownHand

    Winner Straight::v_get_winner(const ShowdownHand& inp_vill) const
    {
        const Straight& vill = static_cast<const Straight&>(inp_vill);

        if (top_rank > vill.top_rank) return Winner::HERO;
        if (top_rank < vill.top_rank) return Winner::VILL;
        return Winner::SPLIT;
    }

    std::vector<Card> Straight::calc_best_hand() const
    {
        const std::vector<Card>& cards = get_cards();
        std::vector<Card> best_hand;
        best_hand.reserve(5);

        CardRank rank = top_rank;
        for (const Card& card : cards) {
            if (card.get_rank() == rank) {
                best_hand.push_back(card);
                --rank;

                if (best_hand.size() == 5) break;
            }
        }

        if (best_hand.size() == 4 && cards[0].get_rank() == CardRank::C_A) {
            best_hand.push_back(cards[0]);
        }

        return best_hand;
    }


    // Flush : ShowdownHand

    Winner Flush::v_get_winner(const ShowdownHand& inp_vill) const
    {
        const Flush& vill = static_cast<const Flush&>(inp_vill);
        const std::vector<Card>& kickers_h = get_best_hand();
        const std::vector<Card>& kickers_v = vill.get_best_hand();

        if (kickers_h.size() != 5) {
            set_best_hand();
        }

        if (kickers_v.size() != 5) {
            vill.set_best_hand();
        }

        return compare_kickers(kickers_h, kickers_v);
    }

    std::vector<Card> Flush::calc_best_hand() const
    {
        std::vector<Card> best_hand = filter_cards(
            get_cards(), [this](const Card& card) { return card.get_suit() == flush_suit; });
        std::sort(best_hand.rbegin(), best_hand.rend());

        if (best_hand.size() > 5) {
            best_hand.erase(best_hand.begin() + 5, best_hand.end());
        }

        return best_hand;
    }


    // FullHouse : ShowdownHand

    Winner FullHouse::v_get_winner(const ShowdownHand& inp_vill) const
    {
        const FullHouse& vill = static_cast<const FullHouse&>(inp_vill);

        if (trips_rank > vill.trips_rank) return Winner::HERO;
        if (trips_rank < vill.trips_rank) return Winner::VILL;

        if (pair_rank > vill.pair_rank) return Winner::HERO;
        if (pair_rank < vill.pair_rank) return Winner::VILL;

        return Winner::SPLIT;
    }

    std::vector<Card> FullHouse::calc_best_hand() const
    {
        const std::vector<Card>& cards = get_cards();
        std::vector<Card> best_hand;
        best_hand.reserve(5);

        for (const Card& card : cards) {
            if (card.get_rank() == trips_rank) {
                best_hand.push_back(card);
            }
        }
        for (const Card& card : cards) {
            if (card.get_rank() == pair_rank) {
                best_hand.push_back(card);
            }
        }

        return best_hand;
    }


    // Quads : ShowdownHand

    Winner Quads::v_get_winner(const ShowdownHand& inp_vill) const
    {
        const Quads& vill = static_cast<const Quads&>(inp_vill);

        if (kicker.get_rank() == CardRank::PLACEHOLDER) cache_kicker();
        if (vill.kicker.get_rank() == CardRank::PLACEHOLDER) vill.cache_kicker();

        if (quads_rank > vill.quads_rank) return Winner::HERO;
        if (quads_rank < vill.quads_rank) return Winner::VILL;

        if (kicker > vill.kicker) return Winner::HERO;
        if (kicker < vill.kicker) return Winner::VILL;

        return Winner::SPLIT;
    }

    std::vector<Card> Quads::calc_best_hand() const
    {
        std::vector<Card> best_hand;
        best_hand.reserve(5);
        best_hand = filter_cards(
            get_cards(), [this](const Card& card) { return card.get_rank() == quads_rank; });

        if (kicker.get_rank() == CardRank::PLACEHOLDER) {
            cache_kicker();
        }
        best_hand.push_back(kicker);

        return best_hand;
    }

    void Quads::cache_kicker() const {
        std::vector<Card> all_cards = filter_cards(
            get_cards(), [this](const Card& card) { return card.get_rank() != quads_rank; });

        kicker = *std::max_element(all_cards.begin(), all_cards.end());
    }


    // StraightFlush : ShowdownHand

    Winner StraightFlush::v_get_winner(const ShowdownHand& inp_vill) const
    {
        const StraightFlush& vill = static_cast<const StraightFlush&>(inp_vill);

        if (top_rank > vill.top_rank) return Winner::HERO;
        if (top_rank < vill.top_rank) return Winner::VILL;
        return Winner::SPLIT;
    }

    std::vector<Card> StraightFlush::calc_best_hand() const
    {
        std::vector<Card> all_cards = filter_cards(
            get_cards(), [this](const Card& card) { return card.get_suit() == flush_suit; });
        std::sort(all_cards.rbegin(), all_cards.rend());

        std::vector<Card> best_hand;
        best_hand.reserve(5);

        CardRank rank = top_rank;
        for (const Card& card : all_cards) {
            if (card.get_rank() == rank) {
                best_hand.push_back(card);
                --rank;

                if (best_hand.size() == 5) break;
            }
        }

        return best_hand;
    }


    // RoyalFlush : ShowdownHand

    Winner RoyalFlush::v_get_winner(const ShowdownHand& inp_vill) const
    {
        throw std::logic_error("RoyalFlush::v_get_winner - Cannot compare two Royal Flushes!");
    }

    std::vector<Card> RoyalFlush::calc_best_hand() const
    {
        std::vector<Card> best_hand = filter_cards(
            get_cards(), [this](const Card& card) { return card.get_suit() == flush_suit; });
        std::sort(best_hand.rbegin(), best_hand.rend());

        if (best_hand.size() > 5) {
            best_hand.erase(best_hand.begin() + 5, best_hand.end());
        }

        return best_hand;
    }

    Winner showdown(const PokerHand& hero, const PokerHand& vill, const Board& board) {
        return eval_hand(hero, board)->get_winner(*eval_hand(vill, board));
    }
}
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Hi @JGrohmann - the Card operator< and other operators are not "stable" ... i.e. given two cards with the same rank but different suit they don't make for a comparison that is logically the same ie. a<b is -b<a \$\endgroup\$ – Mr R Apr 25 at 2:41
  • \$\begingroup\$ @MrR, I see what you mean, thanks for catching that. \$\endgroup\$ – J. Grohmann Apr 25 at 19:32
  • \$\begingroup\$ A quick fix is to use array lookup instead of switch. \$\endgroup\$ – Rick James Apr 26 at 0:47
2
\$\begingroup\$

Your code looks very well written, following many C++ best practices. The only strange things I found is that you don't use auto inside for-statements, for example I would write for (auto &c: cards) instead of for (const Card &c: cards), and some missed opportunities for using std::make_unique. So I'll focus only on the performance aspect of your code.

What should the result of hand strength evaluation be?

The ShowdownHand class is quite heavy. It contains two std::vectors, one for all cards, one for the best hand. And to get the result, you have to go via a virtual function that compares it against an opponent's hand. Is all this information necessary? And if it is, does a copy need to be stored in ShowdownHand if this information should still be in the PokerHand and Board that were used as input for eval_hand()?

Keep the information kept for the result to a minimum, and avoid making unnecessary copies.

Use arrays instead of fixed-size vectors and maps

You have a lot of std::vectors of a fixed, small size. This means there is a lot of overhead from heap allocations and pointer indirections. Use std::arrays instead in these cases.

I also see std::maps being used that will only ever contain just a few elements. Consider for example:

std::map<CardSuit, int> suits;
for (const Card& card : all_cards) {
    ...
    ++suits[card.get_suit()];
}

There are only five possible suits, so it would be much more efficient to write:

std::array<int, 5> suits{};
for (const Card& card : all_cards) {
    ...
    ++suits[card.get_suit_index()];
}

Where get_suit_index() would return a value between 0 and 5. The latter would be trivial if you just made enum class CardSuit have values from 0 to 5. Which brings me to:

Avoid unnecessary conversions

The CardRank class is very weird. It has 14 static members of its own type, functions to convert to/from those static members, a repr member that is not useful for the hand evaluation logic itself. Also, why are val and repr not const? The operator++ and operator-- functions return a pointer to a CardRank, but the class is small enough that it would be better to return a CardRank by value.

While abstraction is good, you should do it in such a way that it is efficient. A CardRank should just be an integer, nothing more. There should be no static instances of CardRanks. The only slow thing should be converting to/from a character representation, and that conversion should only be done during input/output operations, like reading in the board and hand state, or printing those out.

Sorting hands

You have to sort cards in several places in your code. The standard library provides std::sort which is optimized for general usage. However, here we have a special case where we have only a few items to sort, and sometimes we know exactly how many items we have. In this case, std::sort might not be optimal, and a hand-written sorting function might be faster.

You might also investigate whether sorting is necessary in all cases. Does it make sense to sort all_cards at the start of eval_hand()? If so, why do you need to sort again in calc_best_hand(), when it seems like you could skip that and just iterate over that all_cards in reverse, or fill the vector best_hand from the back.

It also looks like sometimes a std::partial_sort() is all you need.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the detailed review, I'll implement these changes and see how they change performance. Aside from what was mentioned here, I also noticed my algorithm for straights can be made more efficient, and a lot of performance was lost allocating and deleting memory after each evaluation, rather than allocating once and reusing the memory. \$\endgroup\$ – J. Grohmann Apr 25 at 19:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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