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?