In Progress
Unit 1, Lesson 21
In Progress

Cycle

Whether dealing cards to poker players or distributing requests to load-balanced nodes, sometimes you need to rotate repeatedly through the same list of objects. In this episode, we’ll first look at a traditional, procedural approach. And then we’ll explore a method from the Ruby standard library for concise, expressive, and flexible turn-taking.

Video transcript & code

Let's say we have a deck of cards.

require "./cards"

DECK
# => [Ace of Hearts,
#     2 of Hearts,
#     3 of Hearts,
#     4 of Hearts,
#     5 of Hearts,
#     6 of Hearts,
#     7 of Hearts,
#     8 of Hearts,
#     9 of Hearts,
#     10 of Hearts,
#     Jack of Hearts,
#     Queen of Hearts,
#     King of Hearts,
#     Ace of Spades,
#     2 of Spades,
#     3 of Spades,
#     4 of Spades,
#     5 of Spades,
#     6 of Spades,
#     7 of Spades,
#     8 of Spades,
#     9 of Spades,
#     10 of Spades,
#     Jack of Spades,
#     Queen of Spades,
#     King of Spades,
#     Ace of Clubs,
#     2 of Clubs,
#     3 of Clubs,
#     4 of Clubs,
#     5 of Clubs,
#     6 of Clubs,
#     7 of Clubs,
#     8 of Clubs,
#     9 of Clubs,
#     10 of Clubs,
#     Jack of Clubs,
#     Queen of Clubs,
#     King of Clubs,
#     Ace of Diamonds,
#     2 of Diamonds,
#     3 of Diamonds,
#     4 of Diamonds,
#     5 of Diamonds,
#     6 of Diamonds,
#     7 of Diamonds,
#     8 of Diamonds,
#     9 of Diamonds,
#     10 of Diamonds,
#     Jack of Diamonds,
#     Queen of Diamonds,
#     King of Diamonds]

To go with our cards, we also have some players.

PLAYER_LIST = %w[Groucho Chico Harpo]

Let's shuffle the deck. For this demonstration, we'll proceed the shuffle with a fixed random seed, just so the examples don't keep changing.

srand(123456)
DECK.shuffle!

We want to deal 3 cards to each player. We'll deal in the order of our player list. So the first card should go to Groucho, the next to Chico, the next to Harpo, then back to Groucho, and so on.

How should we go about this?

Well, we could go with the classic procedural approach.

First, we allocate a list of hands, one for each player, as an array of arrays.

Then we multiply the number players by the number of cards per hand… and set up a loop to run that many times.

Each time through the loop, we select whose hand we are dealing into by taking the current iteration index modulo the number of players. Then we shift the top card from the deck into that hand.

At the end, we have an array of 3 arrays, each with three cards.

To match them up to their players, we zip the players array with the hands array, and convert the final array to a hash. (You can find out more about array-to-hash transformations in Episode #376.)

require "./cards"

hands = Array.new(3){[]}
# => [[], [], []]
(PLAYER_LIST.size * 3).times do |i|
  hands[i % PLAYER_LIST.size] << DECK.shift
end
hands
# => [[6 of Diamonds, 10 of Hearts, King of Diamonds],
#     [4 of Spades, 3 of Diamonds, 6 of Clubs],
#     [10 of Clubs, 8 of Clubs, 6 of Spades]]
PLAYER_LIST.zip(hands).to_h
# => {"Groucho"=>[6 of Diamonds, 10 of Hearts, King of Diamonds],
#     "Chico"=>[4 of Spades, 3 of Diamonds, 6 of Clubs],
#     "Harpo"=>[10 of Clubs, 8 of Clubs, 6 of Spades]}

So, that's one approach. It's an implementation which should be familiar to in its essence any C programmer. But I don't feel like it's the most Ruby-idiomatic way of doing this. And to my mind it isn't terribly intention-revealing. It's more about iteration and math than about distributing cards.

Let's try an alternative approach.

First, instead of using an array of arrays for our list of hands and then converting it to a hash after the fact, let's create the hands variable as a hash of arrays. We'll use the block-form hash constructor that we learned about back in Episode #32, to make a hash where the values will always default to empty arrays.

Then, we'll tell the PLAYER_LIST to cycle through itself 3 times.

To get a feel for what this means, let's just dump the player name inside the block.

When we run this , the output is pretty self-explanatory: the block was executed for Groucho, then for Chico, then for Harpo, then back around to Groucho. And so on, for three full cycles.

If we hadn't specified an argument of 3, the loop would have cycled infinitely.

require "./cards"

hands = Hash.new{|hash,key| hash[key] = []}
PLAYER_LIST.cycle(3) do |player|
  puts player
end

# >> Groucho
# >> Chico
# >> Harpo
# >> Groucho
# >> Chico
# >> Harpo
# >> Groucho
# >> Chico
# >> Harpo

Now that we understand what's going on with the cycle method, let's use it to deal cards. Each time the block executes, we'll select the current player's hand and shift a card onto it from the deck.

And that's all we need to do! When we inspect the results, we see the same hash of arrays we saw before, with three cards each for three players.

require "./cards"

hands = Hash.new{|h,k| h[k] = []}
PLAYER_LIST.cycle(3) do |player|
  hands[player] << DECK.shift
end
hands
# => {"Groucho"=>[6 of Diamonds, 10 of Hearts, King of Diamonds],
#     "Chico"=>[4 of Spades, 3 of Diamonds, 6 of Clubs],
#     "Harpo"=>[10 of Clubs, 8 of Clubs, 6 of Spades]}

I find this version a bit more intention-revealing than the previous approach.

Of course, you may not find yourself needing to use Ruby to deal cards very often. But the cycle method is applicable to a lot of other problems. As just one example: imagine that we're writing a load-balancer. Instead of players, Groucho, Chico, and Harpo are servers, and instead of dealing cards, we're forwarding requests to them. In this scenario, the cycle method is a quick and easy way to implement round-robin request distribution.

And that's it for today. Happy hacking!

Responses