How to Compare Poker Hands in Ruby Using OOP

Hayley Keefer
8 min readDec 15, 2020

--

This article started out as a submission to the unique solutions section of one of the RB120 exercises provided by Launch School, but my response got so long that I decided to write an article. Over the course of three exercises we were asked to create Card, Deck, and finally PokerHand classes. After creating the PokerHand class, the further exploration exercise asked us to think about how we would modify our solution to choose the best hand between two poker hands and to choose the best 5 card hand from a 7 card hand.

Click here for my entire solution and some test cases

In this article I am going to highlight only the methods I created to achieve the tasks outlined above. I will go line by line explaining what’s happening. I find this is helpful for myself to keep track of what type of object we are working with at each method call and thought it might be beneficial to others as well.

Here are all the methods and constants I will be explaining to give you an overview before we start:

Best Hand Between Two Hands

To find the best hand between two poker hands, I first created a constant that references a hash with the hand types (in string form) as keys and a number to represent ranking as values:

I also created a constant that references a hash with the “n of a kind” hand types (in string form) as keys and the number associated with that type (the n in “n of a kind”) as the value:

Then I defined a PokerHand#<=> method that has one parameter, other. I also included the Comparable module in the PokerHand class.

On line 2, evaluate returns the string that represents the hand type (see full code for PokerHand#evaluate method) and assigns it to the local variable hand_type_self. Remember that when we call the PokerHand#<=> method from outside the class definition, the calling object is an instance of PokerHand and any subsequent instance method calls that do not have an explicit calling object are called on that instance. So the call to evaluate can also be thought of as self.evaluate.

On line 3, other.evaluate return the string that represents the hand type of the PokerHand object we pass in to PokerHand#<=>. I didn’t assign this to a local variable because I only use it once in the method.

The strings are used to access the ranking value in the HANDS_RANKING hash using Hash#[]. Integer#<=> then compares these two rankings and the local variable compare now references either 1, 0, or -1.

If the two hands are of different types, compare now references either 1 or -1 and on line 3 we will return compare. If the hands are of the same type, compare references 0 and we need to do some further comparisons to see which is the better hand.

Lines 5–7 cover the case of comparing hands that are of the “n of a kind” variety: four of a kind, full house, three of a kind, two pair, or pair. Ruby will execute line 6 if the keys of the N_OF_A_KIND hash include the string referenced by hand_type_self.

The method that line 6 will call (and PokerHand#<=> will return) if the hands are “n of a kind” is called compare_n_of_a_kind and is passed two arguments. One is other, the PokerHand object we are comparing to the calling object. The second argument is the return of calling Hash#[] on N_OF_A_KIND with an argument of hand_type_self, which is the number (n) associated with the “n of a kind.”

Before showing and explainingcompare_n_of_a_kind, I want to go over a helper method that I will call within compare_n_of_a_kind:

The helper method will be passed an array of Card objects (hand) and a number (n) that represents n in “n of a kind” and returns a nested array. The first inner array contains the Card objects of which the PokerHand object has a count of n. The second inner array contains all the other cards, or kickers.

I was confused when I wrote this line and it actually worked. It’s easy to understand something like [1, 1, 1, 2, 3, 3, 4].count(3). That would return 2because there are two 3’s in the array. But we are passing a custom Card object to count, and the Card objects have different states, so how does count know what to count?

The Array#count documentation offered some insight:

“If an argument is given, counts the number of elements which equal obj using ==.”

Well, the Card#<=> method we defined in our Card class (see full code for method) can be used by Comparable#==, and our definition tells Ruby that Card objects are considered equal (return 0) if they have the same @rank state. So if we pass count a Card object with @rank = 'Jack' and @suit = 'Spades', count checks each Card object in the hand array for its rank and Cards with a rank of 'Jack' are counted.

Here is the compare_n_of_a_kind method that uses split_cards_by_n_kind:

On line 2 of compare_n_of_a_kind, the local variables cards and remaining are assigned to the two inner arrays returned by the helper method. The same is done on line 3 with the local variables other_cards and other_remaining. The difference between the two lines is that split_cards_by_n_kind is passed hand as an argument on line 2 (i.e., self.hand) and is passed other.hand on line 3. hand and other.hand each return the array that the instance variable @hand for self and other references by using the getter method created by attr_reader :hand.

So now the four local variables initialized on lines 2 and 3 point to arrays of Card objects. On line 4, the highest cards in cards and other_cards are compared. The call to <=> here is a call to Card#<=>, since we are comparing Card objects. Likewise, the call to Array#max is using Card#<=> to find the highest card in each array.

The return of calling Card#<=> is assigned to local variable compare, still on line 4. On line 6 the lowest card in each array will be compared if compare points to 0. This covers the case of comparing hands that each have two pairs and the high pairs are equal.

For example, if the calling PokerHand object had card ranks 10, 10, 5, 5, Ace and the PokerHand object passed in (other) had 10, 10, 8, 8, Queen, cards and other_cards would each have 4 Card objects based on split_by_n_kind. Calling Array#max on these arrays will return Card objects with the rank 10 for both card and other_cards, while calling Array#min will return a Card object with rank 5 for cards and rank 8 for other_cards.

Now if compare is still 0, we will compare the kicker(s) for a high card. In the case of a full house, we aren’t really comparing kickers. remaining and other_remaining actually have the Card objects that represent the pair, so calling compare_high_card will decide which pair is greater.

compare_high_card is defined with two parameters, hand1 and hand2, which when called within the compare_n_of_a_kind method will reference the arrays of kicker Card objects. (Later when we compare hands that are not “n of a kind,” we will also call compare_high_card but pass the entire hands to the method, not just a few cards of the hands).

Looking at line 5 first, we see that we access the Card objects in each array referenced by hand1 and hand2 by calling Array#sort (which uses Card#<=>) on the arrays and then using element reference syntax (Array#[]) to return the element at position index. We then compare the Card objects using Card#<=> and store the return in local variable compare.

The index starts at -1 so that we can compare the highest cards in each array with each other first. The index will continue to decrease and compare the next highest cards until we find two corresponding cards that are unequal (i.e., compare is not 0) or until the index reaches -hand.size (i.e., we have compared all cards). Finally, we return compare.

We have now made it through all the methods we need to compare two poker hands. Let’s look at the original method again (which is the only public method out of all the methods shown so far):

We’ve just covered what will happen if the hands are “n of a kind.” If we are comparing any other hand type, line 8 will be executed and we will simply return compare_high_card(hand, other.hand). Like I mentioned earlier, in this case we pass the entire hand arrays to the method by using the getter method for the instance variable @hand. Every hand type besides “n of a kind” can be compared using the high card.

Best 5 Cards

Now that we have a PokerHand#<=> method, creating a method to find the best 5 cards out of 7 will be relatively easy.

The way I approached this method was with the thinking that if you were going to be calling this method from outside the class, you would pass PokerHand.new the hand that you wanted to find the best 5 cards of. The way I have it set up, you can pass PokerHand.new 5 or more cards; it doesn’t have to by 7.

When a new PokerHand object is instantiated and has an array of Card objects passed in, two instance variables are initialized, @cards and @hand. @cards references a copy of the array (using clone) that was passed in and @hand calls a private method deal_hand that returns an array of 5 Card objects “dealt” from the array @cards references (using pop). Now @cards references the array it was initialized to but with 5 less elements than it started with and @hand references an array with 5 elements.

In the following method you will see (hand + @cards), which will return an array with all the cards that were originally passed to PokerHand.new.

combination is called on this array of Card objects with an argument of 5 passed to it. This returns an Enumerator, which we call to_a on. Now we have an array of sub-arrays, with all the combinations of 5 cards that can be made out of all the cards originally passed to PokerHand.new.

Next we call map on this array which will return a new array with each sub-array of Card objects replaced by a PokerHand object. Since we pass the sub-array (hand) to PokerHand.new, this PokerHand object will have an instance variable @hand that references that sub-array of Card objects.

Now we have an array of PokerHand objects. We call max on this array, which uses the PokerHand#<=> method we defined above, and returns the PokerHand object that is the best hand!

Conclusion

Figuring this all out took hours and a lot of debugging, but I am glad I finished because I have a better understanding of OOP now in terms of how to keep track of custom objects and how to define and use a custom <=> method. I hope this was helpful or interesting for you! If you have any refactoring suggestions I would love to hear them.

--

--