CSCI 335 - Artificial Intelligence
Spring 2020
Programming Project #1: Solving Mazes with A*
Overview
You will develop a series of search heuristics for solving a maze.
You will be provided with the code for representing mazes and
generating random mazes. You will develop a best-first search implementation,
along with several heuristics. Mazes can be of different levels of
"perfection", and they can also contain treasures that must be obtained.
Setup
The Java source files are provided in maze.zip.
To set up the files in IntelliJ, do the following:
- Unzip maze.zip in a convenient location.
- Start up IntelliJ.
- Select Import Project.
- Select the unzipped maze directory. Sometimes the extraction process creates an "outer"
directory. Make sure the directory you select has an src folder immediately within it.
- Select Create project from existing sources. Then select Next to navigate the
series of questions it asks.
- Select Yes when asked whether to overwrite the existing project file.
- Select Reuse when asked whether to overwrite the *.iml file.
- Select Finish as soon as possible.
- Open MazeViewer and try to run it.
If you receive any error messages about JUnit, place your cursor on the code highlighted in
red, and type Alt-Enter. This should bring up an option to add JUnit4 to the project.
Programming Assignment
The files are placed in four packages. Classes you will modify are in
boldface.
-
search.core
-
BestFirstHeuristic
interface: All of your heuristics will be classes that implement this interface.
-
BestFirstObject
interface: You will employ an implementation of this interface to represent positions in the maze.
-
BestFirstSearcher
class: As it stands, this class implements breadth-first search. You will modify it to implement best-first search.
-
maze.heuristics
-
BreadthFirst
class: Implements the BestFirstHeuristic
interface.
- You will place all of your implementations of the
BestFirstHeuristic
interface in this package.
-
maze.core
-
Maze
class: Represents a maze.
-
MazeCell
class: Represents a coordinate in a maze.
-
Direction
enum: Represents the four directions of movement in a Maze
.
-
MazePath
class: Represents a path through a maze. It can check to see if the path validly connects the entrance and exit.
-
MazeExplorer
class: Implements the BestFirstObject
interface. It represents an "explorer" collecting "treasure". It does not generate successor "explorers"; you are responsible for implementing this.
-
MazeTest
class: JUnit test class. It is currently failing. If MazeExplorer
is correctly implemented, it should pass testNoTreasure()
and testMany()
. If BestFirstSearcher
is also implemented correctly, it should pass testBestFirst()
.
-
maze.gui
-
MazeViewer
class: This is the main GUI class. This is what you will run to test your heuristics.
-
MazePanel
class: Draws the maze.
-
AIReflector
class: Utility class that finds all of the
heuristics for testing purposes.
Once MazeExplorer
is working, you will need to modify
BestFirstSearcher
to implement best-first search. Once both of
these are working, implement the following heuristics. Each one will be a class that implements the BestFirstHeuristic
interface. Each one
should be placed in maze.heuristics
:
- A heuristic that in some way uses a Manhattan distance calculation
- Two additional monotonic heuristics
- Two non-monotonic heuristics
Note: All heuristics should be completely deterministic. Any
heuristic making use of random numbers will receive no credit,
as a heuristic is supposed to correspond to actual information about the
path to a solution.
Here are some hints and tips to help you with your implementation:
- To get
MazeExplorer
working, you need to pass two unit
tests:
- The first unit test ignores the possibility of treasures. To pass
this test, it should suffice to generate one successor for each
Direction
. Of course, if a given direction is blocked,
or if a neighbor would be outside the maze, then no successor should
be generated for that direction.
- The second unit test takes treasures into account. It will be
necessary to copy over all of the claimed treasures from the parent
node to each successor node. If the successor node contains a treasure,
it will also be necessary to record that fact in that node's set of
treasures acquired.
- To get
BestFirstSearcher
working:
- Replace the queue with a heap. The
java.util
library contains the PriorityQueue
class.
- Make sure that
SearchNode
implements the Comparable<SearchNode>
interface in order for it to work properly with the priority queue.
- You will want to calculate the estimated distance to the goal (h'(n)) in the
SearchNode
constructor, and add an instance variable to store the sum of the
node depth (g(n)) and that heuristic estimate.
- The
compareTo
method in the SearchNode
class can then be
implemented by subtracting the parameter's total estimate from the total estimate for
this
.
- The
testBestFirst()
unit test (in MazeTest
) will check to see if, over a large number of randomly generated mazes, the best first searcher creates fewer nodes than breadth-first search when equipped with a very simple heuristic. If your solution passes this test, you should consider your modifications to be successful.
Proofs
Your proofs need not be fully formal; a well-constructed argument will
suffice. Be sure to employ the
definition of monotonicity from class.
- Prove that the Manhattan-distance heuristic is monotonic.
- Prove that your two additional heuristics are monotonic.
- For each pair of the three monotonic heuristics:
- State which heuristic is better informed, and prove it.
- If neither heuristic is better informed than the other, give two
counterexamples to prove it.
- Prove that your two non-monotonic heuristics are non-monotonic.
Experiments
For each heuristic, determine
experimentally the "hardest" mazes that it can solve in two minutes or less on your
computer. Be sure to specify its clock speed. You will need to determine the
"hardness" of mazes in the following categories:
- Perfect (no loops) to imperfect (very few barriers)
- Amount of treasure to be collected
- Size of the maze
For each experiment you run:
- For each maze you generate, run each heuristic on that maze. This
ensures that comparisons between heuristics are not biased.
- For each maze, record the amount of perfection, number of treasures, and maze dimensions.
- For each heuristic, record the number of nodes expanded, maximum depth reached, solution length, and effective branching factor.
- If a heuristic takes longer than two minutes, feel free to terminate it and
record that, for that experiment, the heuristic took too long.
Paper
When you are finished with your experiments, you will write a short paper
summarizing your findings. Include the following details in your paper:
- A description of each heuristic, and a proof of its montonicity
(or lack thereof)
- The data you collected from each of your experiments
- An analysis of the data, including:
- A discussion of the relative merits of the heuristics relative to
breadth-first search and each other, and
- A discussion as to how varying the size, treasure, and perfection
affects the general difficulty of solving a maze by computer
- Discuss the degree to which the results matched your expectations
- Code for each heuristic
Deadlines
- On Thursday, January 23, come to class with a one-slide PowerPoint
presentation. Briefly describe the heuristics you have already implemented,
in addition to other possibilities you are considering.
- Submit your paper and a zip file containing your code by 2:45 pm on Tuesday, January 28.
Grading criteria
Achievement | Points |
Pass MazeTest.testNoTreasure() | 3 |
Pass MazeTest.testMany() | 2 |
Pass MazeTest.testBestFirst() | 5 |
Implement Manhattan Distance | 3 |
Implement two other monotonic heuristics | 6 |
Implement two non-monotonic heuristics | 6 |
Experiments completed as described | 20 |
Paper describes heuristics and includes source code for each | 5 |
Monotonicity proven (or disproven) for each heuristic | 10 |
Paper describes and analyzes experiments | 10 |