Today's quiz would've been most useful in elementary school, where over half of the homework assignments were word search puzzles. The concept of these puzzles is simple enough that an elementary school student could understand it: given a box of letters, find a line containing the letters of a specified word in order.

For example, find the words ruby, dan, rocks, and matz in the following text:

Notice that the words can go backwards and diagonally, and can intersect one another. Searching is case insensitive.

The word search solver should accept input entered by the user after running the program, i.e., not exclusively through STDIN or a file, by entering the puzzle line by line, pressing return after each line. A blank line indicates the end of the puzzle and the start of the comma separated words to find. The following example shows how a user would enter the above puzzle, with descriptive text from the program removed.

Now, by itself, this quiz is fairly simple, so I offer an additional challenge. Write a beautiful, extensible, and easily-modifiable program without looking at the extra credit before starting. When you're done, try implementing extra credit options using less than 5 or 6 (reasonable) lines of code.

* An output format superior to the one given. The output format given should remain the default unless both formats don't differ on a textual basis. That should sound cryptic until pondered, I can't give too much away!* Allow for "snaking" of answers, in other words, the letters composing a word don't have to be in a straight line.* An option to give a hint, i.e., "The word ruby traverses the bottom left and bottom right quadrants."* Decide what to do with accented letters.* Allow for wildcard letters.

Quiz Summary

I had a few moments this weekend and sat down to work this problem. I was quite surprised to find it tougher than I had expected. I fiddled around for about an hour trying to find a solution I felt was elegant, but never really got there. Luckily, we have many submitters smarter than me.

Almost all of the solutions this week took different approaches and they are all quite interesting. This doesn't seem to be one of those problems we have a standard approach for. Some solutions did a boring search using X and Y coordinates; others started by building a map of points in the puzzle to letters or even letters to points; one even works by performing board transformations. I'm going to show the basic X and Y search in this summary, but the other method were equally interesting and well worth a look.

Let's talk about the solution from Bob Showalter, which begins like this:

ruby

class WordSearch

class Board < Array

def to_s collect {|s| s.split(//).join(' ')}.join("\n")end

end

# ...

Here we see an Array subclass with a trivial modification. Board objects are just two-dimensional Arrays that know how to draw themselves in the quiz output format.

Here we have the initialization for two Board objects. One will hold the actual board, or puzzle object, and the other the solution-in-progress. reset() gives us hints at the solution object structure, which is cleared to a board full of plus characters.

The board is loaded with the following code:

ruby

# ...

# checks that the board contains only letters and that it has a uniform# rectangular shapedef validate@board.size > 0 or raise "Board has no rows"@board.grep(/[^A-Z]/).empty? or raise "Board contains non-letters" w = @board.collect {|row| row.size}.uniq w.size == 1 or raise "Board rows are not all the same length" w.first > 0 or raise "Board has no columns"end

# parses the board by reading lines from io until a blank line (or EOF)# is read.def parse(io = ARGV)@board.clearwhile line = io.gets line = line.strip.upcasebreakif line == ''@board << lineend validate resetend

# ...

validate() is just a reality check for the board. It verifies that we have some rows, those rows contain letters, there are columns in each row, and in fact the same number of columns. That check is run towards the end of parse(), which just reads line by line filling the board object. Note that reset() is also called to prepare the solution object.

The search() method manages the hunt for a single term. It's a trivial brute-force find from every starting square in all eight directions. The method maintains and returns a count, for all occurrences found during the search. The actual letter match is handed off to search_for() though.

In search_for() a recursive search is used to find letter by letter. The tricky part here is that the solution object is modified as we search, assuming we will find the word. This means that an extra walk isn't needed to update the solution later, but it also means the code must revert the changes to the solution object if the word isn't found. Those are the gymnastics you see in the second half of the method.

The next two methods wrap an interface around these calls:

ruby

# ...

# creates a new puzzle by parsing the board from io. see# WordSearch#parsedefself.parse(io = ARGF) obj = new obj.parse(io) objend

def to_s solution.to_send

end

# ...

The class method parse() constructs an object and parses the board from the passed IO object. The instance method to_s() can then be used to show final results, after one or more calls to search().

Here we see the WordSearch object constructed and the board parsed out of the input. The remainder of the input is divided into words and search() is called for each one. A found count is printed for each word as the search is made, then the final solution is displayed.