CSCI 151 - Lab 10 It Boggles the Mind

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:

As usual, you may work with one partner on this assignment, if you choose.

Motivation

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!

Getting Started

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.

Part 1 - Trie

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.

Data Members

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

Constructors

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.)

Public Methods

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.

int size()
Return the number of words stored in this Trie.
boolean containsEmptyString()
Return true if and only if the trie contains the empty string. (This method just returns the isWord flag in the root of the Trie.)
boolean contains(String string)
Return true if the trie contains the given string, false otherwise.
This should use recursion (on the Trie children).
boolean containsPrefix(String prefix)
Return true if the trie contains the edges represented by 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.
This should use recursion (on the Trie children).
boolean add(String string)
Insert string in the trie, if it is not already present. Return true if and only if the trie is modified by this operation. You may need to update size as well.
This should use recursion (on the Trie children).
boolean isEmpty()
Return true if the trie contains no strings, false otherwise.
If you're updating your size class member correctly, this will be the usual one-liner.
String toString()
Return a string representation of the set of strings contained in the trie.
Hint: Use the private method toList() described below; this should get you an arrayList of the strings in the Trie; you can then just return the result of this arrayList's toString method.
Iterator<String> iterator()
Genereate an iterator over all the strings in the trie.
Hint: Lists have an iterator() method that produces what you need! Use your toList method to get you the list you desire.

Private Methods

private ArrayList<String> toList()
Return a List of strings contained in the trie in alphabetical order.
Hint: Use a recursive helper method with a prefix string as an argument (probably with a second argument to store the results). You may have to think about this one for awhile, so give it some time. Build the method up incrementally. It won't necessarily be long (my implementation is 13 lines, and some of those are only squirrely braces), but it may take you a few tries. Ha ha, tries.

Implementation Notes:

  1. 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.

  2. What will happen if the user tries to add a string containing a character outside the range 'a'..'z'? It is likely that an "index out of bounds" exception will occur in the add method. Your program should handle this exception somehow, either in the Trie class or in the application programs you will write which use it. You might want to use Character.isLetter(char ch) or Character.getType(char ch)
  3. You may want to turn all words and guesses into lowercase before performing any Trie operations on them, to make your program as flexible as possible.

Testing with JUnit

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.

Part 2 - Square Class

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.

Part 3 - Boggle Class

Your next task is to write the class Boggle that represents a Boggle board.

Data Members

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!

Constructors

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).

Public methods

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.

Square[][] getBoard()
Return the boggle board.
int numGuesses()
Return the number of guesses in guesses.
String toString()
Return the squares of the board, one row per line of the string.
boolean contains(String word)
Return true if the board contains the word word and false otherwise.
This can be a one-liner if you use your Trie's contains method on the foundWords variable.
boolean addGuess(String guess)
Add guess to the list of guesses, if it is in foundWords.
Return true if it was a valid guess and false otherwise.
void newGame()
Roll the dice and fill the board with new squares accordingly.
Construct a trie for the dictionary words found in the board.
Use the private methods below to do each of these steps.
Remember to rebuild the foundWords and guesses tries.
ArrayList<Square> squaresForWord(String w)
Returns a list of valid Squares on the board that form the word w
It should try to find w starting from each square sq in the board, and return any one list it may find.
This should use the private helper method squaresForWord(sq,w) described below.

Private Methods

void fillDice()
Construct the dice from the file dice.txt.
Each line in the file contains the contents of the 6 sides of the die.
That is, die 0 has the sides L,R,Y,T,T, and E, and so on.
This method should store each line of the file into a different entry of the length-16 array dice.
void fillBoardFromDice()
Construct a new board randomly out of the 16 die.
You want the 16 dice to be placed randomly in the 16 locations, and then each die i to show a random face from dice[i]
You'll want to give some thought to how you find a random shuffling of the 16 dice. It doesn't have to be an efficient shuffle, but it should be somewhat random.
And you should probably have a piece of paper handy on which to draw examples. There will probably be some index arithmetic that you'll want scratch paper for.
Finally, if you roll a "Q" on a dice, you should construct your Square with the String "Qu".
void search(Square sq, String prefix)
Add to the foundWords Trie all words in the dictionary that start with prefix and can be completed in a valid way on the board starting at square sq
See the implementation notes below for a specific algorithm.
void fillFoundWords()
Construct the class member foundWords to contain all words on the board that are in the dictionary.
This should loop through all squares sq in the board, and call search(sq,"") from each one, and adding the Strings of the resulting Trie into the foundWords Trie.
ArrayList<Square> squaresForWord(Square sq, String w)
Return any one valid list of Squares on the board starting with sq that form the word w (or, return an empty list if no such path exists)
This will use similar logic to the search method (although it's not exactly the same).
The method is tricky and will probably need a lot of trouble-shooting, so start small and build on what you know is working.
This will be used to highlight the word on the board.

Implementation Notes:

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

Main method

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!

HandIn

Use handin to submit the following files:

  1. All .java files necessary for compiling your code (including your JUnit tests)
  2. A README file with:
    1. Your name (and your partner's name if you worked with one)
    2. Any known problems or interesting design decisions that you made
    3. The honor pledge
Last Modified: April 8, 2016 by Roberto Hoyle - Originals by Benjamin A. Kuperman, Alexa Sharp, John Donaldson, and beyond VI Powered