Riding the Ruby Bus (Inversion of Control in Ruby)

Playing Cards with Ruby - Part 2

Matthew Maguire
March 25th, 2021

In part one of this series we covered building a deck of cards with Ruby. In this post we will be using the deck that we built to implement a game. We'll start with a simple game that we can play in the terminal and turn it into something that can be integrated into a Discord bot.

Riding the Ruby Bus

The end goal of all this is to make a Discord bot play cards with my friends, and so to achieve this purpose I've chosen a game that is simple and quick to play: Ride The Bus! As an added bonus its a really good drinking game since the dealer almost always wins ;)

The game is a series of three questions that the player must answer about the next card to be drawn in relation to their current hand. If they can guess correctly each time they win! If they answer incorrectly once they lose. The questions that are asked in order are: Black or red? Above or below? Inside or outside?

In the black or red phase the player guesses if the next card to be drawn will be black or red.

In the above or below phase the player guesses if the next card to be drawn will be above or below the first card.

In the inside or outside phase the player guesses if the next card will be inside or outside the first two cards.

So an example game would look like this:

Dealer: Black or Red?
Player: Red
Dealer: Draws an ace of diamonds (a red card)
Dealer: Above or Below?
Player: Below
Dealer: Draws a ten of spades (below an ace)
Dealer: Inside or Outside?
Player: Outside
Dealer: Draws a jack of hearts (inside / between a ten and an ace)
Player: I lose!

Implementing the game

So how do we go about implementing something like this? Here are some things that we need to be able to do:

  1. Display the current turn
  2. Take input from the user about their choice
  3. Draw cards and store previous draws in a hand
  4. Evaluate the drawn card and determine if the player is winning or if they have lost

Ride the terminal

Our initial attempt can be a simple loop where we take user input, draw a card, and evaluate the result. Our turns can be represented by symbols in an array which we iterate over to control the game state. In each iteration we can use a case statement to evaluate logic for a particular turn and exit if the player loses. We can have an additional phase of the game to indicate that the game is over and we should print a final message to the player before exiting.

require './deck'

# initialize a new deck and shuffle it
deck = Deck.new
deck.shuffle!

# we need an array to store the previously drawn cards
cards = []

# each phase
[:black_or_red, :above_or_below, :inside_or_outside, :game_over].each do |phase|

	# exit if game is complete
	if phase == :game_over
		puts 'You won!'
		exit
	end
	# say what phase it is
	puts phase.to_s
	# get some input from the user
	guess = gets.chomp
	# draw a card
	card = deck.draw
	# display what card it was
	puts "#{card.rank} of #{card.suit}"
	# push the card onto our hand for later
	cards.push(card)

	# evaluate based on phase
	case phase
	when :black_or_red
		if (guess == 'black' && card.black?) || (guess == 'red' && card.red?)  
			puts 'You win!'
		else
			puts 'Drink!'
			exit
		end
	when :above_or_below
		if (guess == 'above' && card.rank > cards[0].rank) || (guess == 'below' && card.rank <= cards[0].rank)  
			puts 'You win!'
		else
			puts 'Drink!'
			exit
		end
	when :inside_or_outside

		hand = cards.sort

		if guess == 'inside' && card.rank.between?(hand[0].rank, hand[1].rank)
				puts 'You win!'
		elsif guess == 'outside' && !card.rank.between?(hand[0].rank, hand[1].rank)
			puts 'You win!'
		else
			puts 'Drink!'
			exit
		end
	end
end

# evaluating this looks something like: 
black_or_red
black
six of spades
you win!
above_or_below
above
ace of spades
you win!
inside_or_outside
inside
eight of clubs
you win!

This implementation works well in the context of the terminal, but what if we want to integrate this into some other program (like a Discord bot)? We need to allow the bot to coordinate how and when the game is played.

Inverting the bus

Inversion of control is a principle in programming which is concerned with who initiates messages in a system. There are lots of descriptions of this on the Internet so I wont dig too deep (this one is pretty succinct, ctrl + f 'inversion of control' if you don't care about the rest of the context here). In my mind it mostly boils down to the fact that for a program or piece of code to be useful it often needs to allow other, often unknown, parts of a system control how and when it does what it needs to do. In our first implementation the entirety of "how and when" of the game is defined in the loop construct. If we invert the control of the "how and when" for our game it will be much more flexible. For example maybe we want to have something happen in between turns that the game naturally wouldn't know about? (like handling other Discord messages or whatnot).

So how can we invert control such that when turns are played, inputing guesses, and outputting the game state is controlled by the caller of our game class? Thinking about how we would use the game instance, we want to have an interface that looks something like:

game = RideTheBus.new
game.phase # => :black_or_red
game.play('black') # => true # meaning we won
game.won? # => true
game.complete? # => false
game.cards # => [#<Deck::Card ... > ]

puts 'Such inversion' # now we can do stuff in between turns

game.phase # => :above_or_below
game.play('above') # => true
game.won? # => true
game.complete? # => false
game.cards # => [#<Deck::Card ... >, ... ] 

puts 'Wow!' # and the game doesnt need to know or care what that stuff is

game.phase # => :inside_or_outside
game.play('outside') # => true
game.won? # => true
game.complete? # => true
game.cards # => [#<Deck::Card ... >, ... ]
game.phase # => :game_over

Now the caller of the game object is in control of how and when the game is played. We know how we want to use the game object so lets get implementing!

A Smoother Ride

We can start by sticking this loop into a class and implement some methods which allow an external caller to play turns etc.

require './deck.rb'
class RideTheBus

	def initialize
		@deck = Deck.new
		@deck.shuffle!
		@cards = []
	end

	def play
		[:black_or_red, :above_or_below, :inside_or_outside, :game_over].each do |phase|
			# ...
		end
	end
end

game = RideTheBus.new
game.play
# ... game output

Now we can start a new game on demand by initializing a new RideTheBus instance and calling the play method. However we are still limited to taking input from / outputting to the command line, and every turn happens all at once in the loop.

Enumerable Busses

What we need is a way to control how iteration over phases happens in our game. We want to be able to do one iteration per call of the play method. We can do this by taking advantage of Enumerators and external iteration. Enumerator is the underlying class which handles iterating over objects like an array. This works like below:

# internal iteration
[1, 2, 3].each { |i| puts i } # => 1, 2, 3

# external iteration
enum = [1, 2, 3].each

puts enum.class # => Enumerator

puts enum.next # => 1
puts enum.next # => 2
puts enum.next # => 3

Normally we would pass a block to the each method which will call the block for each element in the array like in the first example. If the block is omitted an instance of an Enumerator is returned allowing us to control the iteration ourselves (this is called external iteration). Calling .next on the instance returns the next element. This allows us to retain our position in the array as the play method is called multiple times.

class RideTheBus
	PHASES=[:black_or_red, :above_or_below, :inside_or_outside, :game_over]

	attr_reader :cards, :phase

	def initialize
		# ...
		@phases = PHASES.each
		@phase = @phases.next
	end

	def play guess
		# ... do game logic /
		# ... case statement

		# at the end of the turn setup the next phase
		@phase = @phases.next
	end
end

This implementation is looking much more flexible, but it still has some issues that we can address. For example, how would we recreate the previous turns in order to display the game history? We need to implement a way to look back at each previous turn / phase and determine what the state of the game was at that point.

State of the bus

We can accomplish this by implementing another class: the Turn class, which can store the state of the game at each turn. Then we can expose these turns so that the calling context can recreate the game state at will.

class RideTheBus
	# ...
	class Turn
		def initialize phase, cards, guess 
			@phase, @cards, @guess = phase, cards, guess
		end

		def won?
			# send the phase symbol
			# which will call one of the 
			# below methods
			send @phase, @guess
		end

		private

		def inside_or_outside guess
			# ... 
		end

		def black_or_red guess
			# ... 
		end

		def above_or_below guess
			# ... 
		end
	end

	# ... rest of RideTheBus class
end

Now let's modify our RideTheBus class to use the Turn class, allow access to previous turns, and thus reconstruct the game state on demand.

class RideTheBus
	# ...

	class Turn 
		# ...
	end

	attr_reader :cards, :turns, :phase
	def initialize
		# ...
		@turns = []
	end

	def play guess
		#...
		
		cards << @deck.draw
		@turns << Turn.new(phase, cards.clone, guess)
		# cloning the cards here ensures that if they are modified the turn's representation of the game state is not modified
		
		@phase = @phases.next
		# ...
	end
end

Each time the play method is called we construct a new turn object an push it onto our turns array. Now it is possible to see our previous turns:

game = RideTheBus.new
game.phase # => black_or_red?
game.play('black')

# ... suppose that we won the game as above
# now we want to print what happened at some other time

game.turns.each do |t|
	puts t.phase
	puts t.cards
	puts t.won?
end
# => black_or_red
# => [#<Deck::Card ... >] 
# => true
# => above_or_below
# => [#<Deck::Card ... >, ...] 
# => true
# => ...

Completing the ride

Now lets implement methods which allow the calling context to determine if the game is being won, and if the game has been completed or not. The implementation is pretty simple. We just need to check if the phase is :game_over and if all previous turns have been won:

class RideTheBus
	#...

	def complete?
		phase == :game_over
	end

	def won?
		@turns.all? &:won?
	end
end

Putting it all together our implementation looks like this:

class RideTheBus
	PHASES=[:black_or_red, :above_or_below, :inside_or_outside, :game_over]

	class Turn
		def initialize phase, cards, guess 
			@phase, @cards, @guess = phase, cards, guess
		end

		def won?
			send @phase, @guess
		end

		private

		def inside_or_outside guess

			hand = [@cards[0].rank, @cards[1].rank].sort

			if guess == 'inside' && @cards[2].rank.between?(*hand)
				true
			elsif guess == 'outside' && !@cards[2].rank.between?(*hand)
				true
			else
				false
			end
		end

		def black_or_red guess
			if guess == 'black' && @cards[0].black?
				true
			elsif guess == 'red' && @cards[0].red?
				true
			else
				false
			end
		end

		def above_or_below guess
			if guess == 'above' && @cards[1].rank >= @cards[0].rank
				true
			elsif guess == 'below' && @cards[1].rank < @cards[0].rank
				true
			else
				false
			end
		end
	end

	attr_reader :cards, :turns, :phase
	def initialize
		@deck = Deck.new
		@deck.shuffle!
		@cards = []
		@turns = []
		@phases = PHASES.each
		@phase = @phases.next
	end

	def play guess
		return if complete?
		
		cards << @deck.draw
		@turns << Turn.new(phase, cards.clone, guess)
		
		@phase = @phases.next if won?
		@phase = :game_over unless won?

		won?
	end

	def complete?
		phase == :game_over
	end

	def won?
		@turns.all? &:won?
	end
end

Finally to demonstrate the concept of inversion of control we can actually go back and re-implement this game for the console using our finished implementation.

require './ride_the_bus'

game = RideTheBus.new

while !game.complete?
	puts game.phase
	game.play(gets.chomp)
	game.cards.each do |card|
		puts "#{card.rank} of #{card.suit}"
	end if game.cards.any?
	if game.won? 
		puts 'You won!'
	else
		puts 'Drink!'
	end
end

Now we are ready to use our RideTheBus game in any context we want (like a Discord or even Twitch bot)

Conclusion

Hopefully you found this cool / interesting and learned a little something about inversion of control and enumerators in ruby. Next time we'll dig into setting up a Discord bot and implementing the game in that environment so we can play cards with our friends / bot and get everyone a little drunk.

How about a share, friend?