Boggle in Ruby: Dice and Grids
I thought I’d make a simple game based on the popular word game Boggle.
The project is available on GitHub, so why not check it out and have a game?
Specifications
If we’re to make a Boggle like game in Ruby, first we’ll need to break down the game into a series of steps that we might be able to do in the program. Here’s how a game of Boggle goes down:
- 16 letters in a 4x4 grid are presented to the players.
- A timer is set for three minutes.
- Players must find words in the grid and write them down in a list.
- After the three minutes are up, players check each of their words to see if they are valid. Here are the rules:
a. The word must be at least 3 letters long.
b. The word must be an actual word, in the dictionary.
c. The letters of the word must be in a chain through the grid, i.e. start at the first letter, and the second letter must be immediately adjacent in any of the surrounding 8 cells, and so on for the third letter.
d. No cell can be used twice in a word. - If the word abides by all the rules, it scores points based on its length, which are added to that player’s total:
3 Letters: 1 point
4 Letters: 1 point
5 Letters: 2 points
6 Letters: 3 points
7 Letters: 4 points
8 or more Letters: 11 points
Now that we know how Boggle works, here are a few things that would be good to include to help the player:
- Display a list of the words the user has already entered.
- Display the grid so the user can always see it.
- After the game, provide the player with a report of their words, and indicate if they were valid.
- Provide a list of words they could’ve got.
Today’s task
That sure is a lot! For today let’s focus on setting up the grid by rolling the dice, and showing that to the player.
Just a note before we begin, all the examples of the grid are going to be the same for consistency in the explanations of what’s going on, but rest assured, when the code is run it will jumble up the dice.
Rolling the dice
We could set up the grid just by setting each element in a 2d array to a random letter of the alphabet, however in real life, the Boggle grid is set by rolling 16 6-sided dice, each with a single letter on each face (apart from “Qu”, which goes on a single face).
I’m sure the configuration of letters on the dice has been carefully thought out so you have enough vowels and the like, so let’s see how we can emulate this.
Here’s an 2d array containing representations of all 16 dice, and in each dice, the six faces (notice the “Qu”):
dice = [
["R", "I", "F", "O", "B", "X"],
["I", "F", "E", "H", "E", "Y"],
["D", "E", "N", "O", "W", "S"],
["U", "T", "O", "K", "N", "D"],
["H", "M", "S", "R", "A", "O"],
["L", "U", "P", "E", "T", "S"],
["A", "C", "I", "T", "O", "A"],
["Y", "L", "G", "K", "U", "E"],
["Qu", "B", "M", "J", "O", "A"],
["E", "H", "I", "S", "P", "N"],
["V", "E", "T", "I", "G", "N"],
["B", "A", "L", "I", "Y", "T"],
["E", "Z", "A", "V", "N", "D"],
["R", "A", "L", "E", "S", "C"],
["U", "W", "I", "L", "R", "G"],
["P", "A", "C", "E", "M", "D"],
]
Now to “roll” all the “dice”, and get that into a new array:
rolls = dice.map do |die|
die[rand(6)]
end
What this does is map through each of the elements of the dice array(die
), and for each die
return a random face, which is then added onto a new array representing the grid.
Let’s snazz it up a bit:
rolls = dice.shuffle.map do |die|
die.sample
endp rolls
# => ["F", "U", "S", "D", "S", "K", "E", "P", "R", "O", "O", "V", "I", "Qu", "U", "H"]
Just before mapping the dice
, it is scrambled with the shuffle method so the dice don’t always appear in the same order. Also, rather getting a random number from 0 to 5 and then accessing the die
array at that index, we can just use the sample
method.
Making the grid
Right now, rolls
is a list, not a grid, so let’s convert it into a 2d array:
grid = []
grid.push(rolls[0,4])
grid.push(rolls[4,4])
grid.push(rolls[8,4])
grid.push(rolls[12,4])p grid
# => [["F", "U", "S", "D"], ["S", "K", "E", "P"], ["R", "O", "O", "V"], ["I", "Qu", "U", "H"]]
That’s a bit more like it! Here, 4 array elements starting from array indices 0, 4, 8 and 12, are each pushed onto the grid
array.
Fortunately we only have four rows to do, but if we had much more this would be really inefficient, so here’s an idea:
rolls.each_slice(4) do |slice|
p slice
end# =>
["F", "U", "S", "D"]
["S", "K", "E", "P"]
["R", "O", "O", "V"]
["I", "Qu", "U", "H"]
The each_slice
method splits the rolls
array into slices with 4 elements each, and then iterates through each slice and inspects it with p
.
However, if we want to assign this to a variable as a 2d array, rather than print it out straight away, we’ll need to do a little more work:
grid = rolls.each_slice(4).to_a
p grid
# => [["F", "U", "S", "D"], ["S", "K", "E", "P"], ["R", "O", "O", "V"], ["I", "Qu", "U", "H"]]
As nothing needs to be done to each slice as it’s iterated over, we remove the block from the each_slice
method, and follow it up with the to_a
method which converts the series of slices into an array, which has been assigned to grid
.
Now to refactor the code into method calledroll_dice
which returns grid
implicitly.
def roll_dice
dice = [
["R", "I", "F", "O", "B", "X"],
["I", "F", "E", "H", "E", "Y"],
["D", "E", "N", "O", "W", "S"],
["U", "T", "O", "K", "N", "D"],
["H", "M", "S", "R", "A", "O"],
["L", "U", "P", "E", "T", "S"],
["A", "C", "I", "T", "O", "A"],
["Y", "L", "G", "K", "U", "E"],
["Qu", "B", "M", "J", "O", "A"],
["E", "H", "I", "S", "P", "N"],
["V", "E", "T", "I", "G", "N"],
["B", "A", "L", "I", "Y", "T"],
["E", "Z", "A", "V", "N", "D"],
["R", "A", "L", "E", "S", "C"],
["U", "W", "I", "L", "R", "G"],
["P", "A", "C", "E", "M", "D"],
] rolls = dice.shuffle.map do |die|
die.sample
end
grid = rolls.each_slice(4).to_a
endnew_grid = roll_dice
Looking pretty good, but the intermediate variable of rolls
and grid aren’t actually needed any more; the whole thing can be done in one line.
def roll_dice
dice = [
["R", "I", "F", "O", "B", "X"],
....
["P", "A", "C", "E", "M", "D"],
] dice.shuffle.map{ &:sample }.each_slice(4).to_a
endnew_grid = roll_dice
p new_grid
# => [["F", "U", "S", "D"], ["S", "K", "E", "P"], ["R", "O", "O", "V"], ["I", "Qu", "U", "H"]]
This is the same as before, but rather than assigning to the roll
variable or the grid
variables all the methods are chained on in one line and the resulting array is returned implicitly. The do end
block has been replaced by this: { &:sample }.
The ampersand creates a block with the symbol of the sample
method.
Displaying the grid
At the moment the grid isn’t very nice to look at, what with it being full of braces and quote marks, and it being on one line and all. It would be much nicer if it could look like this:
+-----+-----+-----+-----+
| F | U | S | D |
+-----+-----+-----+-----+
| S | K | E | P |
+-----+-----+-----+-----+
| R | O | O | V |
+-----+-----+-----+-----+
| I | Qu | U | H |
+-----+-----+-----+-----+
Perhaps we could achieve this with string interpolation?
puts "+-----+-----+-----+-----+"
puts "| #{grid[0][0]} | #{grid[0][1]} | #{grid[0][2]} | #{grid[0][3]} |"
puts "+-----+-----+-----+-----+"
puts "| #{grid[1][0]} | #{grid[1][1]} | #{grid[1][2]} | #{grid[1][3]} |"
puts "+-----+-----+-----+-----+"
puts "| #{grid[2][0]} | #{grid[2][1]} | #{grid[2][2]} | #{grid[2][3]} |"
puts "+-----+-----+-----+-----+"
puts "| #{grid[3][0]} | #{grid[3][1]} | #{grid[3][2]} | #{grid[3][3]} |"
puts "+-----+-----+-----+-----+"
The individual array elements are interpolated into the string longhand with a series of #{grid[0][0]}
. This works, but the syntax isn’t sugary enough for my taste. Here’s an alternative:
puts row = '+-----+-----+-----+-----+'
grid.each do |line|
puts "| #{line.join(' | ')} |"
puts row
end# =>
+-----+-----+-----+-----+
| F | U | S | D |
+-----+-----+-----+-----+
| S | K | E | P |
+-----+-----+-----+-----+
| R | O | O | V |
+-----+-----+-----+-----+
| I | Qu | U | H |
+-----+-----+-----+-----+
The row
variable is assigned to the row separation string, and is puts
ed as it’s assigned. Iterating throughgrid
, each line
is joined with the join
method, delimited by a string with some whitespace for padding and a pipe (|), which is interpolated into another string for the outside pipes as the edges of the grid, and after each line another row is puts
ed.
It’s almost perfect, but can you spot the problem? Yes, that pesky “Qu” is messing up the spacing.
puts row = '+-----+-----+-----+-----+'
grid.each do |line|
puts "| #{line.map{|c| c.ljust(2)}.join(' | ')} |"
puts row
end# =>
+-----+-----+-----+-----+
| F | U | S | D |
+-----+-----+-----+-----+
| S | K | E | P |
+-----+-----+-----+-----+
| R | O | O | V |
+-----+-----+-----+-----+
| I | Qu | U | H |
+-----+-----+-----+-----+
Before calling the join
method, here map
is called on the line, and each character (c
) is given trailing whitespace to extend it to 2 characters by the ljust
(left justify) method. The delimiting whitespace on join
is adjusted to compensate too.
Let’s refactor this into a method called display
:
def display(grid)
puts row = '+-----+-----+-----+-----+' grid.each do |line|
puts "| #{line.map{|c| c.ljust(2)}.join(' | ')} |"
puts row
end
end
Nice.
Getting classy
What if we want to have more than one game on the go? Let’s refactor everything into a class called Boggle
:
class Boggle
def initialize
@grid = @@dice.shuffle.map{ &:sample }.each_slice(4).to_a
end
def display
puts row = '+-----+-----+-----+-----+'
@grid.each do |line|
puts "| #{line.map { |c| c.ljust(2)}.join(" | ")} |"
puts row
end
end@@dice = [
['R', 'I', 'F', 'O', 'B', 'X'],
['I', 'F', 'E', 'H', 'E', 'Y'],
['D', 'E', 'N', 'O', 'W', 'S'],
['U', 'T', 'O', 'K', 'N', 'D'],
['H', 'M', 'S', 'R', 'A', 'O'],
['L', 'U', 'P', 'E', 'T', 'S'],
['A', 'C', 'I', 'T', 'O', 'A'],
['Y', 'L', 'G', 'K', 'U', 'E'],
['Qu', 'B', 'M', 'J', 'O', 'A'],
['E', 'H', 'I', 'S', 'P', 'N'],
['V', 'E', 'T', 'I', 'G', 'N'],
['B', 'A', 'L', 'I', 'Y', 'T'],
['E', 'Z', 'A', 'V', 'N', 'D'],
['R', 'A', 'L', 'E', 'S', 'C'],
['U', 'W', 'I', 'L', 'R', 'G'],
['P', 'A', 'C', 'E', 'M', 'D'],
]
endnew_game = Boggle.new
new_game.display# =>
+-----+-----+-----+-----+
| F | U | S | D |
+-----+-----+-----+-----+
| S | K | E | P |
+-----+-----+-----+-----+
| R | O | O | V |
+-----+-----+-----+-----+
| I | Qu | U | H |
+-----+-----+-----+-----+
The roll_dice
method has been split in two, with the dice
variable now @@dice
, a class scope variable, so it can be accessed by any instance of the class. The rest of the method is now part of the initialize
method, which is run when a new instance of the class is initialised. The display
method is the same, just referencing the instance scope@grid
variable, meaning the variable in this instance rather than one belonging to a different instance.
Great! Now we can play a game of Boggle even if all we have is Ruby installed, and some paper and pens to write down our words.
Thanks for reading! Check out the next part here.