Programming Assignment 10: Search
Estimated reading time: 15 minutes
Estimated time to complete: two to three hours (plus debugging time)
Prerequisites: Assignment 09
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 without uploading to Gradescope more times than is reasonable.
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 will cover 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 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 Cell
s, 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 the list of cells: [(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 that implement
this interface.
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.
There are two tasks to accomplish; they are mostly independent of one another. You must implement the missing methods in Searcher
, and you must implement the EightPuzzle
search problem.
Implementing Searcher
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.
Implementing EightPuzzle
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. (In past semesters, 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.