Camille Baldock

Poker Ranking in Ruby

September 10, 2014 | 12 Minute Read

I was the second guest on Drew Neil’s Peer to Peer interview series, hosted by Tom Stuart. I was given a programming problem about poker card-ranking. Tom Stuart sat by me and asked me questions about how I was approaching things as I worked on the problem. I had lots of fun filming that video: you can watch the episode here.

I have made the code from the session, as well as how I approached refactoring it later is available on Github.

Problem description

A “hand” in poker consists of five playing cards drawn from a standard deck. Implement a system which can decide which of two hands has the highest ranking, according to the standard poker ranking rules.

Tools

Editor

I’ve been a IDE/editor nomad these past few months. I used to write code mostly in C# and Java, so coming from VisualStudio and IntelliJ my first text editor when switching to Ruby was Rubymine. I then paired with some vim lovers (check out Sam Phippen’s dotfiles, that at the time had little snowmen replacing trailing whitespace and made the whole experience strangely poetic. Also thanks to Stephen and Tim for reactivating those arrow keys when I was driving :-) ).

I’ve recently been pairing with some Sublime Text users so that was my editor of choice for the Peer to Peer session.

Programming language and libraries

I jumped straight into Ruby and RSpec without further explanation. Those are the tools I am using the most in my current projects so it just felt natural to me.

Typing and speaking

Thinking, typing and speaking about what you’re typing all at once is difficult. It was slightly off-putting at times since I usually tend to speak a bit less when I’m “driving” in a pair, and more when I am not.

Shortcuts!

Most of my Sublime shortcuts rely on the Shift key…and mine split that morning so a lot of my transitions felt quite forced or missed a few times.

split key

I think jazz piano has made me a slightly too vigorous typist.

Steps in the video

The steps will make a lot more sense if you have watched the video. The code for the session and the refactoring that followed it is available on Github.

Why start with parsing?

When watching the video, it struck me that I just jumped straight into writing a hand parser, instead of starting with how I would compare two hand objects, without much explanation.

The reasons why I chose that approach were:

  • I felt upfront that a lot of the rules I was going to implement were going to be a pain to think of completely abstractly: writing a test double representing each hand indicating each suit and number, indicating equal numbers etc… looked like it could be verbose.

  • I was also approaching the problem from the point of view of a user of a system. It felt somehow more natural and easy to me for a user to feed in ["5h", "6h", "7d", "8s", "9c"] to my ranker rather than:

[
  {
    :pips => 5,
    :suit => :hearts,
  },
  {
    :pips => 6,
    :suit => :hearts,
  },
  {
    :pips => 7,
    :suit => :diamonds,
  },
  {
    :pips => 8,
    :suit => :spades,
  },
  {
    :pips => 9,
    :suit => :clubs,
  },
]

However, a bit of upfront design of the interface I wanted for my Hand object would have spared me a bit of the lack of enthusiasm for the resulting structure of that HandParser object.

Issues with the HandParser object interface

At several points during the video, I comment and make notes to myself for future refactorings because I dislike what my HandParser object looks like. I don’t elaborate much on that point although I think it is one of the interesting object design questions in that exercise.

There are many things that I dislike about the HandParser object made in the video, but here’s a quick list:

  • I am tying myself to a specific format of hand input (array of strings)

  • I am tying myself to a specific format of card input (string with pips then suit: “5h”)

This is a textbook case of not designing my object with the open-closed principle in mind.

I am also thoroughly unconvinced about how well my responsibilities are separated: the parse method ends up being not just a parser but also a Hand initialiser, which is not a satisfactory design outcome.

I am however happy that all that parsing logic is staying out of my Hand object. How my user chooses to represent a Hand, and the validation of that input, should not be part of its initialisation logic! This was an attempt at keeping the responsibilities of input-parsing and hand comparison separate.

Following the video, I have refactored that interface to address the concerns above: if you want to see how I move away from that questionable design decision, please see below in “The Road Ahead”.

On-the-go refactoring

A few compromises had to be made in choosing whether I wanted to stop and refactor my objects and methods so I would be happier with the code, or keep trying to solve the problem as much as I could.

I chose the latter which means I ended up with a codebase filled with annotations on how to improve things: I have refactored that code since and addressed some of those concerns. The end of that video feels like “shameless green” in places, but is definitely not code I’d be happy shipping as is.

Shortcuts and annotations

Over a few years of coding, I have developed my own annotations and notes to remind myself of possible improvements and shortcuts I have taken that need to be addressed before the code is ready to go.

##bob is a slightly silly notation I use when I have a method/class/variable with a name that does not please me (not descriptive enough, actually wrong, or leaves too much room for confusion) but I can’t think of a better name at time of writing. It is a reference to Bob Martin (Uncle Bob) and his Clean Code book.

A quick look through some of my commit histories reveal my most popular annotations to be:

  • #TODO
  • #WWSMD: What would Sandi Metz do?
  • #bob

The road ahead

Leaving a codebase unfinished with so many annotations on how to fix and improve things made me want to come back to it to: - Address design issues - Test my ranking with a large poker data set found online. - Implement full comparison (not just comparing types of hands, but deciding which hand wins between two hands of the same type).

Code at the end of the session

Setup continuous integration and a README

First things first! I make sure I have a Travis build in place to know if I inadvertently break my tests, and setup a README for documentation of the project and the refactoring process to come.

Finish implementing all the poker hands

In a “shameless green” style, I implement straight flush and two-pairs types of hands that were not covered during the filmed session.

Fix some of the code duplication in the Hand object

I address the ##DUPE! pip_count comment left in the Hand object during filming. The pip_count and suit_count methods are essentially duplicates of each other.

Improve some method names

I address the ##bob comment left in the Hand object.

Move some array sorting and checking logic to an ArrayHelper

I address the #TODO: live in a helper, utility comment left in the Hand object. The consecutive_cards? methods was mainly checking whether an array is made of consecutive integers. The logic for this wasn’t card-specific and is now moved to an ArrayHelper.

Better test names!

As you may see in the video, the tests are speedily copy-pasted from each other to go through as many hands as possible quickly. This left me with several specs with identical names, which makes the file difficult to read, and the errors difficult to interpret. I am fixing that with the following commit.

Start implementing better failures for the HandParser

I address the #TODO: fail nicely when not 5 cards comment. The HandParser now gets initialised with an array of card strings and fails if the array is the wrong size.

Apply the open-closed principle to parsing and making hands

I finally address the #TODO: this is not a nice interface comment.

This revolves around changing the initialisation for a Hand to be:

def initialize(card_input, hand_parser)
  @cards = hand_parser.parse(card_input)
end

Apply the open-closed principle to card string parsing and create a CardParser

In the same way as the commit above, I don’t way to be tied to a specific method of parsing cards for my HandParser. After the following two commits, the HandParser gets initialised with a card_parser argument, which means it is no longer tied to the exact format of the string to represent a given card.

Comparable interface

My initial quick attempt at comparing hands was having a better_than? method on hand that would accept another hand. There is a more elegant way to do such things in Ruby that involves using Comparable.

Integration script with real-world data

Integration script to check I am ranking poker hands correctly, based on the data found at http://archive.ics.uci.edu/ml/datasets/Poker+Hand.

Refactoring

In a more realistic coding situation, I tend to stop and refactor as I go. Some might say it’s therapeutic.

Refactor aces as ones logic

Choosing not to implement royal flush: just another straight flush

Implement tie-breaker for straight hands

In this commit, I begin to return more details about hands so that I can compare them successfully in case of a tie. - Implement highest for straight hands

Refactor #rank

Therapeutic refactoring…

Tie-breakers for two pairs and four of a kind

Refactor pip and suit counting in hands

Therapeutic refactoring…

Tie-breakers for all hands

Implement for each hand all the tie-breaking information needed to rank them: this is different for each hand.

Compare all hands

Use the extra information returned to compare hands of the same type correctly.

Refactor comparisons

Therapeutic refactoring…

Final clean-up

Conclusion

I stopped at this point in the exercise! I am content that the code actually solves the problem that Drew and Tom set for me, as well as a lot more satisfied about the design of my objects.

If you have any feedback or suggestions on this exercise, I’d love to hear about it or why not:

Pair program with me!