Due by 10:00pm, Sunday May 05
In this lab, you will use tries to implement the family-fun game of Boggle.
The purpose of this lab is to:
The game Boggle is played with 16 dice that have letters on all faces. The dice are randomly deposited into a four-by-four grid so that the players see the 16 letters on the top faces. For example, the board may look like this:
Each player has a limited amount of time to identify as many words as can be located on the board. Words can be formed using adjacent letters in any direction (including diagonals) but cannot reuse letters or wrap around the board. For example, the board below contains the words dent, hit, tine, tide, hub, bun, hide, raid, rain, etc. The location of "tide" is highlighted in blue. Players accumulate points based on the number of words found.
In this lab, you are going to write a program that lets the user play a one-person version of Boggle. The GUI is provided for you; your job is to write the program that rolls the dice, reads and stores a dictionary, finds all dictionary words on the board, and checks whether given guesses are on the board. There's plenty to do, so let's get on with it!
The lab10.zip file contains several dictionary files: enable.txt (probably the best one to use for the finished project), words_ospd.txt (the official scrabble dictionary), lexicon.txt (a smaller but still extensive dictionary), and small.txt (a much smaller dictionary). It also contains dice.txt (a list of the dice), Square.java (to be explained later), ExpandableList.java, and BoggleFrame.java (the Boggle GUI). You should not have to edit any of these files in this lab.
In today's lab you'll implement your own Trie class called MyTrie, which should be a subclass of AbstractSet<String>. If you need it, you can read these lecture notes for details on the use and implementation of Tries.
Each Trie instance should contain the following data members:
boolean isWord; // whether this trie node is the end of a word int size; // the number of words represented by this trie MyTrie[] children; // the children tries of this node
The no-argument constructor for a Trie should initialize an empty Trie. The result should be a single Trie node whose isWord flag is false and whose children array is an array of null pointers. (You need to instantiate the array, but not its individual entries.) Recall that the size of the array is determined by the number of characters in the alphabet; in our case, that will be 26. (This is a good place for the use of a "static final" variable to define a constant, so that you don't need to hard-code the number 26 in more than one place in your program.)
Remember: you should be testing these methods as you construct them, one at a time. Create MyTrieTest.java to contain your JUnit tests and add to it as you go along. This will save you time down the road.
Note that most of these are recursive and refer to the trie rooted at the current node. For example, t.contains( "bob" ) is true if t.children[1].contains("ob") is true.
prefix
, false otherwise. Note that this is not
checking isWord
values, just existence of the
appropriate children. As soon as one of the expected children
is not present, return false.
In searching a trie for a string, the individual characters of the string must be used to index into each trie node's array of children. To accomplish this, you will need to convert each character to a numeric index. In particular, you will need to convert 'a' to 0, 'b' to 1, 'c' to 2, ..., and 'z' to 25.
This is easy to do in Java, because characters are considered to be a numeric type compatible with int, so it is possible to perform arithmetic operations on them. When Java performs arithmetic on characters, each character is interpreted as the number used in its Unicode representation. Because the letters of the alphabet are assigned consecutive Unicode values, a letter can be converted to a number in the range 0..25 by simply subtracting the letter 'a' from it. For example, 'b' - 'a' is equal to 1, 'c' - 'a' is equal to 2, 'd' - 'a' is equal to 3, etc.
Note: Use actual character literals. Use of numeric equivalents will result in a loss of points. For example, use 'z' instead of 122 or 0x7a or 0172.
Recall that String has the toLowerCase() and charAt() methods that might be useful when performing these calculations.
Character.isLetter(char ch)
or
Character.getType(char ch)
You should thoroughly test your Trie class before proceeding. For example, a good test may create a new empty Trie, then add the words "hello", "hellos", "hella", "apples", "bolivia", and "bologna". You could then check whether your trie contains "hello" (it should), "h" (it should not), and "hellos" (it should). If you print out your tree, the words should appear in alphabetical order. Of course, you'll want more tests than just this, but it's a start.
Another good test is as follows: Build a trie from a lexicon file (some are provided for you or you can write your own), and display it using toString. Then use the contains method to test a bunch of the search words.
As discussed, a Boggle board consists of a four-by-four grid of dice. Therefore, there is a Square class provided to you that represent a square on the boggle board; that is, the "showing" side of one of the dice. You can read through the class to see how it works---it's not very complicated. Don't worry about "marking" the Square yet; that will become clear in the next portion of the lab.
Your next task is to write the class Boggle that represents a Boggle board.
Each Boggle board should contain the following data members:
MyTrie lex; // The dictionary, stored in a Trie Square[][] board; // The 4x4 board MyTrie foundWords; // The dictionary words on the current board MyTrie guesses; // The valid words made so far by our one player String[] dice; // An array of dice -- explained later!
You should write a Boggle constructor that takes a single String parameter that
represents the file name of your lexicon (i.e. dictionary), and creates a new
Trie out of the words in that lexicon. You should also "initialize" the dice
(see the private fillDice
method).
If you try to implement all the methods below before testing them with the GUI, you will probably have a tough time debugging your code. To alleviate some pain and suffering, I recommend that you implement all the methods (or, the non-trivial ones) with "bogus" default code (the minimum amount needed to make the compiler happy), then add the implementations one at a time, running your Boggle program as you go along.
The public methods are mostly short. Of the private methods, you need fillDice( ) and fillBoardFromDice( ) to get anywhere with the game. After those are working you can add search( ) and fillFoundWords( ). If you make squaresForWord( ) return a list containing just board[0][0] the game will accept correct guesses (it checks guesses against the foundWords trie), but it won't light up the correct dice. Finally, add the correct versions of squaresForWord( ) to complete the game.
guesses
.
word
and false otherwise.
contains
method on the foundWords
variable.
guess
to the list of guesses, if it is in foundWords.
w
w
starting from each square sq
in the board, and return any one list it may find.
squaresForWord(sq,w)
described below.
dice
from the file dice.txt.
dice
.
i
to show a random face from dice[i]
prefix
and can be completed in a valid way on the board
starting at square sq
foundWords
to contain all words on the board that are in the dictionary.
sq
in the board, and call search(sq,"")
from each one, and adding the Strings of the resulting Trie into the foundWords Trie.
sq
that form the word w
(or, return an empty list if no such path exists)
search
method (although it's not exactly the same).
For this lab, whenever you see a "q" on the board, you should treat is as "qu".
Here is a nice search algorithm to find all words on the board. It is based on the idea of "recursive backtracking", which we've already seen in our maze solver lab.
for each square sq on the board search for words starting at sq in lex via the helper method
The search helper method uses the following logic:
search(Square sq, String prefix) // check to see if we have found a word on the current path if the current path represents a word in the dictionary add the word to the wordlist // continue searching on all possible paths from this square if there are any words possible from this prefix (use containsPrefix()) let l = the letter in sq for each unmarked square s adjacent to sq mark it recursively search for words starting at square s with prefix prefix+sq.letter, and add these to wordlist unmark it
Your Boggle class should have a main method that takes a single command-line argument that is the file name of the lexicon. Your program should then construct a new Boggle instance from this argument, and create a new BoggleFrame instance as follows:
Boggle boggle = new Boggle( args[0] ); BoggleFrame bFrame = new BoggleFrame( boggle ); bFrame.pack(); bFrame.setLocationRelativeTo(null); bFrame.setVisible(true);
And now you should be able to run your program with the command
% java Boggle small.txt
or
% java Boggle enable.txt
and play Boggle to your heart's content.
Alternatively you can run this within Eclipse, giving it a command-line argument such as enable.txt.
The dictionaries take a few seconds to
load, and this happens in between the GUI showing up and the dice letters
loading, so be patient, but not too patient. (You can always try out
Serializable and writing the Trie to disk to speed things up.) Enjoy!
Use handin to submit the following files: