daniel baird dot com

Ruby bingo cards - part 2

For my wife’s last baby shower, I used Ruby to make cards for a bingo game. This is a walkthrough of that process, which might be interesting if you’re new to Ruby. In this second part, I take the code we wrote in the first part and turn it into a useful class.

DRAFT

This post is Under Review. You’re allowed to read it, but let me know if there’s something wrong.

Wrapping it up in a class

As soon as you need more than one of something, it’s generally worth making a class for it.

Like most programming languages that have classes, a Ruby class is a kind of template that objects can be based on. That’s great for making lots of things. I’ll need many bingo cards, so I’ll take the code from part one, put it into a BingoCard class, and then I can make lots of bingo cards.

Here’s what a Ruby class looks like:

class BingoCard
  # ..methods defined here..
end

If you make an initialize method in your class, Ruby will run that to set up new instances of your class. So that’s the perfect place for the random-number-picking Ruby code from part one.

Source code

class BingoCard

  def initialize
    @card = []
    5.times do
      row = []
      5.times do
        row << rand(25)
      end
      @card << row
    end
  end

end

I’ve made one tiny change to the code from part one: I’m using @card instead of just card. Using an @ in front of a variable like @card makes it an instance variable, meaning the variable will continue to exist inside the object, instead of disappearing at the end of the current method.

That’ll make more sense after this next bit.

The other code we wrote in part 1 was the printing part. Here it is pasted into our BingoCard class:

Source code

class BingoCard
  # ------------------------------------
  def initialize
    @card = []
    5.times do
      row = []
      5.times do
        row << rand(25)
      end
      @card << row
    end
  end
  # ------------------------------------
  def print_card
    @card.each do |row|
      row.each do |square|
        print square.to_s.center 5
      end
      puts
    end
  end
  # ------------------------------------
end

I’ve made one change here as well: instead of giving the method a card to print (a_card), I’m just printing @card. @card belongs to this object, and each method inside the object gets to see that same variable. That’s why we used @card in the initialise method — so we can refer to it here. Keeping track of “inner” variables like @card is one of the reasons objects are useful.

Also, I’ve added some commented lines of dashes between the class methods. That’s not required by Ruby or anything, it’s just a weird quirk I picked up ages ago. Feel free to just leave a blank line or two if you prefer that.

Classes are templates for making actual things

Okay so now I’ve written a class, we can make objects that “have” that class. Like this:

Source code

class BingoCard
  # ...
end

my_card = BingoCard.new  # make a new BingoCard
my_card.print_card       # print it

Result

 11    1    8    3    2
 23    9   18    3   18
  3   10    7    7   14
 21    5   18    4   13
  8    2   19   22   15

Now we’ve re-created the results of part 1, but all neatly wrapped up in a class. Compared to part one, though, it’s now super easy to make and print out several cards:

Source code

class BingoCard
  # ...
end

my_card = BingoCard.new
your_card = BingoCard.new
another_card = BingoCard.new

puts "Mine:"
my_card.print_card

puts "Yours:"
your_card.print_card

puts "Theirs:"
another_card.print_card

Result

Mine:
 15   13   20   23   13
  8    4   19   17   18
  3    5   11   17   16
  8    4    1   13   20
 19   14   10   20   13
Yours:
 10    5   17   23    3
 15   15    0    3    5
 14    3    6   10    3
 15    3    0   12   14
  1   11    6    1   12
Theirs:
 18   15   17    7    3
 14   21   23   14    6
 14    9    3    8    7
  1   20    3    8   10
 24   14   12   20    8

Notice that each card has its own version of the @cards data structure. Yay objects. Also notice that it’s slightly awkward to say my_card.print_card. Ruby code should be beautiful and graceful, like leaping gazelles silhouetted against the sunset as a light breeze stirs the long grass of the veldt. So, I’ll rename the print_card method to just show.

Source code

class BingoCard
  # ------------------------------------
  def initialize
    # ...
  end
  # ------------------------------------
  def show
    @card.each do |row|
      row.each do |square|
        print square.to_s.center 5
      end
      puts
    end
  end
  # ------------------------------------
end

my_card = BingoCard.new
your_card = BingoCard.new
another_card = BingoCard.new

puts "Mine:"
my_card.show

puts "Yours:"
your_card.show

puts "Theirs:"
another_card.show

Result

Mine:
  4   13   16    2   24
 15    4   21   20   17
  8    4    3   20   19
 16    5    3    2   24
  1    2   16   21    6
Yours:
 24   16   17   14   16
  7   11   10   11   20
 18    7   10    5    2
 12   12    7    8    8
 11   23    1    6    4
Theirs:
 22    3    8   11   17
 12   21   24    2    9
 16   10   19   23   18
  9   17    1   11   23
 11    7   15   22    1

Use words not numbers

Alright, where are we? Reviewing my feature list from part 1, I can see a few problems with our output:

  • Sometimes the same number shows up twice (or thrice, or, um.. quice or something?);
  • There might be draws, where multiple boards win with the same five things;
  • The boards have numbers instead of cute baby-related photos.

Bingo cards that look pretty, with all baby pictures on, is a good job for HTML. We can just put image tags with different img URLs into a HTML table — so this card maker thingy should probably deal with image URLs instead of numbers. Next, I’ll take a step towards that by changing the code to put strings (that could be filenames or URLs or whatever) into the squares instead of numbers. We’ll deal with the HTML layout later.

Here’s an array of strings in Ruby:

Source code

items = ['baby','rattle','teddy']
puts items

Result

baby
rattle
teddy

I’ll modify the initialize method to take an array, and use the items in the array to populate the card’s squares.

Source code

  def initialize items
    @card = []
    5.times do
      row = []
      5.times do
        row << items[rand items.length]
      end
      @card << row
    end
  end

So now when you call BingoCard.new, you have to give it an array of strings. Even though you’re calling new, the arguments you give it make their way through to your initialize method. In my initialize method, when I add a square to a row, I’m picking a random item from the array.

Use it like this:

Source code

class BingoCard
  # ...
end

my_card = BingoCard.new ['a','b','c']
puts "Mine:"
my_card.show

Result

Mine:
  b    c    a    c    b
  b    a    c    b    c
  c    c    c    a    c
  c    c    b    a    a
  c    c    a    b    b

Yep that’s working. But we need at least 25 things, which is, like, sooooo muuuch typing. So here’s something fancier:

Source code

class BingoCard
  # ...
end

my_card = BingoCard.new [*'a'..'z']
puts "Mine:"
my_card.show

Result

Mine:
  o    c    p    w    s
  u    v    c    k    d
  y    m    z    i    q
  q    m    r    v    i
  y    r    w    h    y

See the [*'a'..'z']? That’s a bit interesting, and I’m going to not-quite-explain it in two parts. Firstly, Ruby understands ranges. 1..10 is a range that starts at 1 and goes to 10. 'a'..'z' is a range that goes from letter a to letter z.

Secondly, you can put an asterisk * in front of a Ruby range to turn it into a list. When it’s used this way an asterisk is called a ‘splat’, and it’s pretty weird.

If you haven’t seen splat before, this might be a bit confusing. For now, just trust me: if you splat a range and wrap the whole thing in square brackets, you get an array of all the values in that range.

Alright, so we’re passing in a list of strings and using those for the values of our bingo card squares. But they’re still being repeated. Let’s fix that now.

Here’s where a random item is picked out of the item list:

        row << items[rand items.length]

That line picks a random number up to the length of the array, and gets the thing at that position. If we also removed the thing from the list, it wouldn’t be in the list for next time.

Just like a perfect girlfriend, Ruby wants you to be happy. In this case, Ruby’s Array class aready has exactly the method we want: delete_at, which deletes an item from the array while also returning the deleted item. So we can drop in a delete_at like this:

        row << items.delete_at(rand items.length)

Run the program a few times to see if you get any repeated items. No? Awesome. Remember the features I listed in the “Framing the problem” section of part 1? One was No repeats, and we’ve just achieved it. We’ve also added a small, subtle bug — if you can’t see it yourself, you’ll need to wait for part 3 before I explain it.

Extra credit

Nervous that a single letter isn’t enough? Try making your own item list, like this:

Source code

my_card = BingoCard.new [*'abc'..'def']

Or like this:

Source code

item_list = [
  'dummy',
  'blanket',
  'easter bunny',
  # ..think up more, until you have at least 25!
]
my_card = BingoCard.new item_list
puts "Mine:"
my_card.show

You might need to change your show method, which currently assumes squares will be up to 5 letters across. Here’s the current version — see if you can fix it:

Source code

  def show
    @card.each do |row|
      row.each do |square|
        print square.to_s.center 5
      end
      puts
    end
  end

This article

by Daniel Baird first appeared here on 13 August 2013.

Topics