Programming Assignment 09: Search

Estimated reading time: 15 minutes
Estimated time to complete: 90–120 minutes (plus debugging time)
Prerequisites: Assignment 08, Lab 05
Starter code: search-student.zip
Collaboration: not permitted

Overview

In this assignment, you will implement a search algorithm. Given a problem, your implementation will not only find a goal, but find a complete solution — a path to a goal. You will also implement a solution validator. You’ll develop these implementations on a search problem we provide you (a maze), but you’ll then model a simple puzzle (the 8-puzzle) as a search problem yourself.

We’ve provided a set of unit tests to help with automated testing. The Gradescope autograder includes a few more tests, including tests you do not have access to. We have left the names of these tests visible as a hint to what they’re testing, but you will likely need to write some tests of your own to complete this assignment.

Note that if you run into trouble with the Eclipse debugger mysteriously quitting during unit tests, it’s due to the timeout rule that we use to catch infinite loops:

@Rule
public Timeout globalTimeout = Timeout.seconds(10); // 10 seconds

Comment out the above two lines in all test files, and the debugger will no longer exit (and test cases can now get stuck in infinite loops).

Goals

  • Show understanding of search and search problems in the context of a partial view of a problem.
  • Show understanding of a solution validator for a general search framework.
  • Practice writing unit tests.
  • Test code using unit tests.

Downloading and importing the starter code

As in previous assignments, download and save (but do not decompress) the provided archive file containing the starter code. Then import it into Eclipse in the same way; you should end up with a search-student project in the “Project Explorer”.

On search

Search is a powerful and general technique for problem solving. Problems in many domains yield to search: scheduling, routing, constraint satisfaction, optimization, optimal game play, navigation, and planning, to name a few. In lecture, we showed how to implement search on the general graph abstraction, where you have total knowledge of the graph before you start searching. For this assignment, you will be implementing search on a slightly different abstraction.

Most search problems can be represented as graphs, where states in the problem correspond to vertices, and edges exist between vertices if one state is a successor of the other. For example, in the game of chess, each possible arrangement of pieces is a state. As in most search problems, there is a single starting state. There are 20 successors to this state, each corresponding to one possible move by the first player (two for each pawn and two for each knight).

But there is a practical problem: Chess has an estimated 10123 states that can be reached through legal play. Building the entire graph to represent such a large state space is infeasible. Chess is a simple game and causes us this difficulty; many more complex search problems likewise have too large of a state space to fully translate to a graph.

But, we needn’t necessarily create the entire graph. Instead, we can start at just the initial state, and look at its successors, and theirs (and so on), stopping when we reach a goal state. That is, we can simplify a search problem to three components:

  • an initial state: the initial configuration of the problem we are trying to solve
  • a goal test: given a state, this test determines if it is what we are searching for (for example, in chess, a board where we are victorious)
  • a method to find a list of successors for a given state: given a state, enumerates the valid successors for this state (for example, in chess, the boards that result from each possible move from the current board)

In this assignment, you will adapt the breadth-first search algorithm we presented to this framework.

Examining the code

Take a look at the code in the project file. We’ll describe the important bits here.

The graphs package is (a subset of) a general graph implementation, similar to the code we covered in class. It’s used to to build mazes, one of the search problems your code will be solving in this assignment.

Important: You do not need to, and should not, call any code in graphs in your solution! It is only included to support the maze generator!

The integers package contains a simple search problem, FindIntegersProblem, which corresponds to one of the homework problem’s you’ve done. It searches the number line, integer by integer, looking for a specific goal integer: either a positive integer or a negative integer. You might want to practice your test-writing skills by writing a few “smoke tests” for this class, in the same style as the tests in SearcherTest.

The mazes package contains code to build random mazes (in MazeGenerator) and to represent them (in Maze). A maze consists of Cells, which represent (x, y) coordinates in the maze (where the upper left is (0, 0) and the lower right is (width - 1, height - 1)).

The Maze class has a toString method which you may find helpful. Here is an example output of toString on a Maze of width and height three:

    #0#1#2#
    0  S  0
    # # # #
    1     1
    # ### #
    2  G  2
    #0#1#2#

The starting cell (1, 0) is marked with an S; the goal cell (1, 2) is marked with a G. Cells that are adjacent (that is, are successors of one another) have empty space between them, and cells that are not have a wall, represented as #, between them. The borders of the maze contain the x coordinate (modulo 10) along the top and bottom, and the y coordinate along the left and right.

In this maze, one possible solution is (1, 0); (0, 0); (0, 1); (0, 2); (1, 2). This path represents the starting cell, a move left, a move down, a move down, and a move right, to the goal cell.

The search package contains classes related to the general implementation of search. The SearchProblem interface describes a search problem and the type of its associated state; Maze is a complete example of a search problem. Searcher is a class describing the general functionality that will be required by any search implementation that operates on a SearchProblem. Notice that Searcher.findSolution doesn’t just report a goal state was found: it finds and returns an explicit List of states, from the initial state to a goal state. Finally, Solver is a utility class that allows you to instantiate a single object and use it to solve a SearchProblem.

Last, the puzzle package contains a stub class EightPuzzle, that you will use to build an implementation representing a new search problem, representing the 8-puzzle (a simplified version of the 15-puzzle; see https://en.wikipedia.org/wiki/15_puzzle.

What to do

There are a few small tests already, which you will likely want to add to. You may also want to write an interactive driver — the main methods in MazeDriver, FindIntegersDriver, and EightPuzzle should give you an idea of how to use the various classes together.

A Searcher should be able to validate that the solution it found was valid. Start by implementing the isValidSolution method. Then move on to the findSolution method.

Once you have that working, the main method of the MazeDriver will run to completion, and show you the results of a search on a random maze. You can vary the maze by changing the width, height, or seed. You can also test against the other drivers, as noted above, but again: I strongly recommend you write some tests rather than caveman debugging your way to victory here.

Once you have your searcher working, it’s time to implement a new search problem. Turn your attention to EightPuzzle. You should fill out each of the stub methods here. getInitialState and isGoal should be trivial, depending upon how you choose to represent a game state within EightPuzzle. getSuccessors will require that you understand the rules of the game, and return the complete set of successors of a given state, as a List<List<Integer>>.

Other notes

The default timeout set in the tests we’ve provided you suffices for those tests. But if you write your own tests involving larger search spaces, it may require that you change the timeout to be larger.

Half of all possible EightPuzzle states will never lead to a solution. When testing, you might want to start with hand-crafted instances you know are solvable, rather than randomly generating them.

We will test your solvers on problems with no valid solution (that is, no path from the initial state to a goal); make sure you return an empty list (not null) in these cases.

As usual, you can add new private methods to the classes in src/, and you should complete the methods marked TODO, but you must not modify the method signatures of public methods, or change files in support.

Submitting the assignment

When you have completed the changes to your code, you should export an archive file containing the src/ directory from your Java project. To do this, follow the same steps as from Assignment 01 to produce a .zip file, and upload it to Gradescope.

Remember, you can resubmit the assignment as many times as you want, until the deadline. If it turns out you missed something and your code doesn’t pass 100% of the tests, you can keep working until it does.