How to Compare Poker Hands in Ruby Using OOP
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.
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 2
because 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 Card
s 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.