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":/articles/ruby-bingo-part-one/ and turn it into a useful class.
This post is Under Review. You're allowed to read it, but "let me know":about if there's something wrong.
h2. 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:
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.
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:
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.
h2. 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:
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:
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@.
h2. 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:
I'll modify the @initialize@ method to take an array, and use the items in the array to populate the card's squares.
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:
Yep that's working. But we need at least 25 things, which is, like, sooooo muuuch typing. So here's something fancier:
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:
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:
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.
h2. Extra credit
Nervous that a single letter isn't enough? Try making your own item list, like this:
Or like this:
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:
by Daniel Baird first appeared here on 13 August 2013.