Inspired by Matthew Jones series I thought it would be fun to try modelling a simple deck of playing cards, just for fun and of course practice. Matthew mentions in one of his articles:

[…] the difficulty in creating complex software programs is not writing the code, but in getting the correct requirements.

In my experience as a writer of code and a manager of people who write code, I have witnessed this to be entirely accurate. For software developers, writing code is comfortable and poorly defined requirements are not. You can't build what you don't understand so we have meetings with other humans to try and gain some more understanding, to reduce the unknowns.

The point of this exercise is to reduce the unknowns by modelling a well defined domain, a standard 52-card deck (with 2 jokers, so 54 cards technically). I'm starting these exercises with a deck of cards because I hope to try building some card games in the future and being able to reference this session will be helpful.

Bedsides modelling the cards themselves, we'll also look at an interface to the deck so we can perform various real-world interactions you might expect such as: shuffling, drawing cards and cutting the deck. I'll be using Ruby for this, it's the language I'm most familiar with right now so I can focus on the problem of modelling and not the language features.

Just want to review the code? It's over on GitHub.

The standard 52-card deck

What makes up your standard 52-card deck? In case you aren't familiar there are 4 Suites: Spades ♠, Clubs ♣, Hearts ♥ and Diamonds ♦. Each Suite has the same set of 13 Ranks: Ace, 2 through 10, Jack, Queen and King.

Then of course we have 2 Jokers, these are wild cards not used in all card games but are often included in a pack of cards.

Writing the first test

For this exercise I'm going to try take a strict test driven approach, that means I will work out the test first before writing any other code. I'll be using MiniTest as my testing library.

If I had a real deck of cards in my hand right now and I wanted to check if it was a full deck quickly, I might just count how many cards there were. We can use this idea to write our first test:

require "minitest/autorun"

class CardDeckTest < MiniTest::Test
  EXPECTED_NUMBER_OF_CARDS = 54
  def test_deck_has_correct_number_of_cards
    card_deck = CardDeck.new
    assert_equal EXPECTED_NUMBER_OF_CARDS, @card_deck.size
  end
end

This is a very basic test but that's what we want right now as we don't even know if our test works yet. Let's write this CardDeck class exist and satisfy the test:

class CardDeck
  def size
    54
  end
end

This is the minimum implementation required to pass our test but I'm not confident we have a deck of cards here just yet! While we don't have a deck of cards yet the test does serve to tell us our test suite is working.

A full deck of cards

We need another test, one that allows us to check if every card in a standard deck exists, not just the total number.

Again I encourage thinking about how we might do this in the real-world? With a real deck of cards in hand, you might iterate through the deck, look at each card and check each one off against a known list of cards. To do this in our code we'll need a couple of things:

  1. A trusted list of cards we expect to find in the deck.
  2. A way to iterate through the cards in our deck.
  3. Protection against tampering with the deck. For example: in the real-world, we wouldn't want to drop any cards as we went through the deck so we need to protect against mutations to our deck
  4. Some way to represent the cards that make up the deck.

Starting at the top, we need a list we can trust and a method for iterating the deck.In Ruby, this is commonly represented as an #each method so we'll keep that convention.

class CardDeckTest < MiniTest::Test
  # [...]

  EXPECTED_CARDS_IN_SUITE = %w(A 2 3 4 5 6 7 8 9 10 J Q K).freeze
  EXPECTED_CARDS = {
    "♠︎" => EXPECTED_CARDS_IN_SUITE,
    "♣︎" => EXPECTED_CARDS_IN_SUITE,
    "♥︎" => EXPECTED_CARDS_IN_SUITE,
    "♦︎" => EXPECTED_CARDS_IN_SUITE,
    "*" => %w(Joker Joker).freeze,
  }.freeze

  def test_it_contins_a_full_deck_of_cards
    card_deck = CardDeck.new
    actual_cards = Hash.new { |hsh, key| hsh[key] = [] }
    card_deck.each do |card|
      actual_cards[card.suite] << card.rank
    end

    assert_equal EXPECTED_CARDS, actual_cards
  end
end

The expectations are "hard-coded" and the collections frozen. For me this helps make the tests clearer to reason about as I don't need to run some Ruby code in my head to work out what is being created. It also reduces the risk of human error through errors in any generating code we might have written. Freezing the collection prevents modifications (as much as is possible in Ruby anyway).

A priority of any test is be clear and easy to reason about.

We have a failing test again 🥳 and a plan for the public interfaces we need to build:

class CardDeck
  def initialize
    @cards = [].tap do |cards|
      %w(♠︎ ♣︎ ♥︎ ♦︎).each do |suite|
        %w(A 2 3 4 5 6 7 8 9 10 J Q K).each do |rank|
          cards << Card.new(suite, rank)
        end
      end
      2.times { cards << Card.new("*", "Joker") }
    end
  end

  def each(&block)
    @cards.each(&block)
  end

  def size
    @cards.size
  end
end

class Card
  attr_reader :suite, :rank

  def initialize(suite, rank)
    @suite = suite
    @rank = rank
  end
end

You'll notice I also updated the #size method to check against the @cards array we built. A Card class was also introduce to hold and expose the #suite and #rank.

The Card class does not have any "setter" methods and our #each iterator does not support changes to the @cards array so I'm confident that this is not exposing obvious means to tamper with the cards via the public interface. This satisfies point 3 from the list above.

Interacting with a deck of cards

Now there is a deck of cards "in our hand" but it's not very useful in it's current form. We should create an interface for some of the common actions you would perform on a deck of cards so we can support using the deck in a game.

Shuffling

It would be a pretty predictable game of cards if the deck could not be shuffled. The Fisher–Yates shuffle is an optimal shuffling algorithm. Not only is it unbiased, but it runs in linear time and uses constant space. The algorithm is easy to implement however, Ruby already provides it for us through Array#shuffle. To test our shuffle we can compare:

  • two unshuffled decks are the same.
  • an unshuffled deck and a shuffled one are different.
  • two shuffled decks are not the same.
class CardDeckTest < MiniTest::Test
  # [...]
                                   
  def test_the_deck_is_shuffled
    unshuffled_deck_1 = CardDeck.new
    unshuffled_deck_2 = CardDeck.new
    assert_equal unshuffled_deck_1, unshuffled_deck_2

    shuffled_deck_1 = CardDeck.new.shuffle!
    refute_equal unshuffled_deck_1, shuffled_deck_1

    shuffled_deck_2 = CardDeck.new.shuffle!
    refute_equal shuffled_deck_1, shuffled_deck_2
  end
end

For the above test, a method of asserting equality between 2 decks is required through the #== method if we are to compare the 2 decks. While the introduction of this method to the public interface is a convenience for testing only at this time, I felt it was acceptable. The cards array is shuffled in place and will mutate the @cards array, indicated by the use of bang! variation of the #shuffle method.

class CardDeck
  # [...]

  def shuffle!
    @cards.shuffle!
  end

  def ==(other_deck)
    cards_from_other_deck = []
    other_deck.each do |card|
      cards_from_other_deck << card
    end

    @cards == cards_from_other_deck
  end
end

class Card
  # [...]

  def ==(other)
    suite == other.suite && rank == other.rank
  end
end

Our comparison method collects all the cards from the other deck into an array and then we allow the Array#== to effeciently compare each item of the arrays in turn.

Drawing cards from the deck

During a game of cards it's likely players will need to be dealt cards or draw them from the deck. To do this, we'll want to remove the card from the collection and return it via the method. Since we don't have sides to our cards we'll just assume we want to take the card off the top of the deck.

class CardDeckTest < MiniTest::Test
  # [...]

  def test_a_card_can_be_drawn_from_the_deck
    card_deck = CardDeck.new.shuffle!
    drawn_card = card_deck.draw!
    deck_size_after_draw = card_deck.size

    assert_equal (EXPECTED_NUMBER_OF_CARDS - 1), deck_size_after_draw

    card_deck.each do |card|
      refute_equal card, drawn_card, "Drawn card found in remaining deck"
    end
  end
end

In this test we expect the size of our deck to be reduced by 1 and for the drawn card to be absent from the deck.

class CardDeck
  # [...]

  def shuffle!
    @cards.shuffle!
    self
  end

  def draw!
    @cards.shift
  end
end

A slight change was needed to the #shuffle! method, it was returning the @cards array and not the CardDeck instance we wanted. The Array#shift method we use in #draw! removes the first card from our array and returns it. Easy.

Cutting the deck

Finally, we might also want to "cut the deck", this means to split the deck (roughly) in half typically. From there, each half can be cut further if needed. The deck may not be cut exactly in half so we'll need to allow for a way to specify where the deck is cut.

class CardDeckTest < MiniTest::Test
  # [...]
                                   
  def test_deck_can_be_cut
    card_deck = CardDeck.new
    number_of_cards = 10
    size_of_right_cut = (EXPECTED_NUMBER_OF_CARDS - number_of_cards)

    left, right = *card_deck.cut!(number_of_cards)

    assert_equal number_of_cards, left.size
    assert_equal size_of_right_cut, right.size
  end
end

We are expecting an array of decks to be returned and for them to be the expected size. Passing this test is going to require some changes to our initializer as we will need a new deck instance with only the cards from one side of the split.

class CardDeck
  def initialize(cards = build_cards)
    @cards = cards
  end

  # [...]

  def cut!(num_of_cards)
    [
      CardDeck.new(@cards[0, num_of_cards]),
      CardDeck.new(@cards[num_of_cards, @cards.size])
    ]
  end

  private

  def build_cards
    [].tap do |cards|
      %w(♠︎ ♣︎ ♥︎ ♦︎).each do |suite|
        %w(A 2 3 4 5 6 7 8 9 10 J Q K).each do |rank|
          cards << Card.new(suite, rank)
        end
      end
      2.times { cards << Card.new("*", "Joker") }
    end
  end
end

After being cut, the deck is likely to be put together again so we should have a way to do that so we can control that interface. Doing this will also allow us to check if we get the same deck back after a cut.

class CardDeckTest < MiniTest::Test
  # [...]

  def test_a_cut_deck_can_be_rejoined
    card_deck = CardDeck.new
    number_of_cards = 10

    left, right = *card_deck.cut!(number_of_cards)
    assert_equal card_deck, (left + right)
  end
end

The passing code for our test is as follows:

class CardDeck
  # [...]

  def +(other_deck)
    cards_from_other = []
    other_deck.each do |card|
      cards_from_other << card
    end

    CardDeck.new(@cards + cards_from_other)
  end

  # [...]
end

With the ability to instantiate CardDeck from an array of cards it is trivial to collect all the cards together and return a new CardDeck instance.

Of course this being Ruby we have no idea that the array we've provided to a CardDeck actually contains Card at all, if that's a concern then a strongly typed language would provide some confidence there.

Now I am familiar with the problem I could use it to try out a new language to get familiar with a new syntax and features without also having to think about the problem I am trying to solve.

As mentioned at the start of the article, full code is available over on GitHub.