Building a Deck of Cards in Ruby

Playing Cards with Ruby - Part 1

Matthew Maguire
November 4th, 2020

In the time of Covid we are all connecting more remotely, and for me and my friends that meant starting a Discord server as a virtual hangout. While Discord is awesome as is we can make it cooler by implementing a bot to interact with. As a side project in this vein I decided to make this bot play cards games with my friends and keep track of their scores. This was a really cool project and I wanted to share the implementation and what I've learned along the way, from the deck of cards, to implementing a game, to integrating with the bot itself. In this post and the few to follow I'll document how I created the card abstraction, the game I decided to go with, and how I integrated it with the bot.

A Deck of Cards

To start with we need a few simple classes which represent a deck of cards. What are some things we would need to be able to do with a deck of cards, and with the cards themselves? The goals of this implementation are as follows:

  1. Create a new deck.
  2. Shuffle or sort the deck.
  3. Draw cards from the deck.
  4. Compare between cards.

Implementing the deck is very simple; its just a wrapper around an array which stores our cards:

	# deck.rb
	class Deck
		attr_reader :cards
		def initialize
			@cards = [] 
			# create the cards in our deck
		end

		def draw
			@cards.pop
		end

		def shuffle!
			@cards.shuffle!
		end

		def sort!
			@cards.sort!
		end
	end

Here we define a deck class. Its initialize method just creates an empty array to store our cards in the draw, shuffle! and sort! methods call methods on the cards array. Now we need to implement a card class and fill our array of cards with instances of the class.

Implementing cards

Now we need to implement a card class to fill our deck. So what is a card? Well a card has two main properties: its rank, and its suit. In this sense a card is a good example of a Value Object. A quick Google tells us that a Value Object is a simple object whose equality is based on some values and not identity. Without straying too far into a discussion of value objects this means that an instance of a Card is equal to another instance when their rank and suit values are the same.

We can show how this works by using symbols (which are value objects themselves) to represent rank and suit values, and the Struct class to stand in for our card class:

	Card = Struct.new(:rank, :suit)

	jack_of_diamonds = Card.new(:jack, :diamond)
	another_jack_of_diamonds = Card.new(:jack, :diamond)
	jack_of_diamonds == another_jack_of_diamonds # => true

	jack_of_spades = Card.new(:jack, :spade)

	jack_of_spades == jack_of_diamonds # => false

This works out of the box since structs in Ruby are equal when they are of the same struct subclass and equal member values. Now lets implement our own card class:

	# card.rb
	class Card
		attr_reader :rank, :suit
		def initialize rank, suit
			@rank, @suit = rank, suit
		end

		def == other
			rank == other.rank && suit == other.suit
		end
	end

	# now we can compare instances of our card class
	Card.new(:jack, :spade) == Card.new(:jack, :spade) # => true
	Card.new(:ace, :spade) == Card.new(:jack, :spade) # => false

But what if we want to do more than say whether two ard are equal? For example, to be able to say whether one card is less than another card? What does it mean for a card to be less than another card? Well in most card decks the ranks and suits are ranked in order from lowest to highest. If we store the symbols we are using to represent rank and suit values in an array we can then tell how they compare:

	# card.rb
	class Card
		SUITS = [:spade, :heart, :diamond, :club] # this is the usual way
		RANKS = [:ace, :king, :queen, :jack, :ten, :nine, :eight, :seven, :six, :five, :four, :three, :two]

		attr_reader :rank, :suit
		def initialize rank, suit
			@rank, @suit = rank, suit
		end

		def == other
			rank == other.rank && suit == other.suit
		end

		def > other
			#lower position in the array means higher value
			RANKS[rank] < RANKS[other.rank] && SUITS[suit] < SUITS[other.suit]
		end

		def < other
			RANKS[rank] > RANKS[other.rank] && SUITS[suit] > SUITS[other.suit]
		end
	end

	# now we can compare instances of our card class
	Card.new(:jack, :spade) == Card.new(:jack, :spade) # => true
	Card.new(:ace, :spade) == Card.new(:jack, :spade) # => false
	Card.new(:ace, :spade) > Card.new(:jack, :spade) # => true
	Card.new(:ace, :spade) < Card.new(:jack, :spade) # => false

Rank and Suit classes

Implementing these methods we can see that ranks and suits are actually value objects in their own right. In order to simplify the implementation we can create rank and suit classes which handle comparisons between themselves

	#rank.rb
	class Rank
		RANKS = [:ace, :king, :queen, :jack, :ten, :nine, :eight, :seven, :six, :five, :four, :three, :two] 
		attr_reader :rank
		def initialize rank
			@rank = rank
		end

		# ... operators ommitted
	end

	#suit.rb
	class Suit
		SUIT = [:spade, :heart, :diamond, :club]
		attr_reader :suit
		def initialize suit
			@suit = suit
		end

		# ... operators ommitted
	end

	Suit.new(:spade) == Suit.new(:spade) # => true
	Suit.new(:spade) == Suit.new(:diamond) # => false

	Rank.new(:ace) == Rank.new(:ace) # => true
	Rank.new(:ace) == Rank.new(:king) # => false

Now we can re implement our card class as a composition of these smaller value objects.

	# card.rb
	class Card
		attr_reader :rank, :suit
		def initialize rank, suit
			@rank, @suit = Rank.new(rank), Suit.new(suit)
		end

		def == other
			rank == other.rank && suit == other.suit
		end

		# ...
	end

include Comparable

Now to tie this all together: if we want to do lots of comparisons between our cards and perform operations on an array of them (like shuffle in our deck implementation), we can include the Comparable module. The Comparable module is a mixin included by classes whose objects may be sorted. It requires that we implement the <=> operator in order to get a bunch of comparisons. To do this we need to return -1, 0, or +1 depending on whether the card is less than, equal to, or greater than the other card. So lets get at it:

	#rank.rb
	class Rank
		# ...

		def <=> other
			return 0 if rank == other.rank
			RANKS.index(rank) < RANKS.index(other.rank) ? 1 : -1
		end
	end

	#suit.rb
	class Suit
		# ...

		def <=> other
			return 0 if suit == other.suit
			SUITS.index(suit) < SUITS.index(other.suit) ? 1 : -1
		end
	end

	#card.rb
	class Card
		# ...

		def <=> other
			return 0 if rank == other.rank && suit == other.suit
			return 1 if rank >= other.rank && suit >= other.suit
			return -1
		end
	end

Now we can finally return to our deck implementation and fill our card array with Cards!

	# deck.rb
	class Deck
		attr_reader :cards
		def initialize
			@cards = Rank::RANKS.flat_map do |rank|
				Suit::SUITS.map do |suit|
					Card.new(rank, suit)
				end
			end
		end

		# ...
	end

	deck = Deck.new # => # <Deck:0x000000000104f280 @cards=[ Some cards here... ]>'
	deck.suffle! # => # <Deck:0x000000000104f280 @cards=[ Some sorted cards here... ]>'
	deck.draw # => # <Card:0x0000000000fa4a38 @rank=#<Rank:0x0000000000fa4998 @rank=:ace>, @suit=#<Suit:0x0000000000fa4948 @suit=:spade>>' 

Hope you found this a little bit interesting! Stick around for the next couple posts to see how I went about building a card game and integrating it into a discord bot :)

How about a share, friend?