Programming Assignment 09: Search

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

NOTE: Portions of this assignment depend upon lecture material we will cover on April 4th, so you’ll have until April 11th to complete this assignment.

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.

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 in lecture 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 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 problems you’ll be assigned (or have done, before this assignment is due). 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; FindIntegerProblem and Maze are complete examples 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. You’ll adapt the code we wrote in lecture to implement the Searcher (along with a little more code you’ll write yourself).

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, which will strongly resemble the code we wrote in class (no need to reinvent the wheel here!).

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

If you write your own tests involving larger search spaces, the search might not terminate in a reasonable amount of time. But for any solvable instance of the problems we’ve provided, the search should terminate very quickly. (Last semester, a few students had timeouts due to odd choices in data structures in their findSolution method; if you think this is happening to you, upload to Gradescope, ask on Piazza, and we’ll look at your code.)

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.