1 Guidelines
1.1 Contracts & Test Cases
2 Assignment
3 Representing Mazes
4 Implementing Maze Generation
5 Implementing Maze Solving
6 Displaying Mazes
7 Implementation
8 Getting Started
9 Bugs or Corrections
10 The Racket Version

Assignment 4, CSC102, Spring 2012

1 Guidelines

1.1 Contracts & Test Cases

For this and all remaining assignments, every method you develop must come with the following things:

2 Assignment

In this assignment, you’ll develop functions that generate and solve mazes.

Put all of your classes in a package called assignment4 .

Please make all of your methods and constructors public, to allow testing.

You may choose to develop a plugin for Minecraft that creates your maze as pathways in the sky, but it’s not required.

The following few sections provide a high-level description of the functionality you must implement. Following this is a more low-level look at the classes you’ll need to implement.

3 Representing Mazes

As we discussed in class, we can represent mazes as "places", indexed by an x and y coordinate, and a set of "connections", or wall openings, connecting one place to another, along with a "start" place and a "goal" place. A "path" can be represented as a list of places.

4 Implementing Maze Generation

In a MazeUtils class, implement the generateMaze method. It should accept a width and height in cells, a start place and a goal place, and return a Maze.

I’m going to describe what I see as the most straightforward way to generate mazes. You’re welcome to choose a different algorithm, as long as it has the same signature. Also, I’ve attached my Racket code to the bottom of this assignment; although Racket is not a language you’re familiar with, it may nevertheless be useful to you to read a working implementation of the program.

Generating the maze involves generative recursion. Specifically, maze generation involves extending a set of paths, branching sometimes, until the grid is full, and no paths can be extended any more.

At each step, we must
  • pick a path to extend,

  • choose some subset of the adjacent unused cells,

  • for each such new cell, add a path to the set of paths that has the new cell on the front of the existing path,

  • mark all of the newly taken cells as no longer unused,

  • trim from the path list those that have no adjacent unused cells any more.

For more detail, see the code below for the extend function.

When all of the paths have dead-ended, we’re done.

5 Implementing Maze Solving

In the Maze class, implement the object-oriented solve method, that returns a solution path for a maze, or signals an exception if there is no solution.

Maze solving is quite similar to maze generation. In this case, the generative recursion halts when we find a path that reaches the goal location.

In this case, we’re going to keep track of a set of paths. At each step, we’ll remove one path from the queue, figure out all the ways to extend it, taking care not to go through walls or re-visit places, and add the extended paths to the bottom of the queue. Again, see the code below for the extend-solution-path function.

6 Displaying Mazes

It’s all fine and well to generate and solve mazes, but if you have no way of displaying the mazes, it all feels a bit abstract. Develop the display method on the Maze class that prints a maze in a simple textual form, where spaces indicate places and openings, and the letter O indicates walls or wall intersections. There should be a solid frame of wall around the maze. The a period (.) should indicate the start, and the letter ’X’ should indicate the goal. When the two are the same, you can show either one. That’s not a very interesting maze, though.

Use the function System.out.println to display a line of text.

Here’s a simple example of a 4x4 maze. Note that the ’O’s representing the walls fall in between the cells:

OOOOOOOOO

O O.    O

OOOOO OOO

O       O

OOOOO O O

O     O O

O O OOO O

O O   OXO

OOOOOOOOO

In order to make this function testable, also implement the toDisplayLines method that returns a List<String> containing the lines of the output above (not including the newlines) so that calling System.out.println on each one in turn produces the diagram above. In fact, the easy way to develop this is to develop this one first, and then have display call it.

Also, develop the method displayWithSolution that draws a solution path using periods. Here’s the same example as before, drawn with solution path:

OOOOOOOOO

O O...  O

OOOOO.OOO

O    ...O

OOOOO O.O

O     O.O

O O OOO.O

O O   OXO

OOOOOOOOO

As for display, you must implement a testable toDisplayLinesWithSolution method (sorry about the name) that returns the lines of text as a list of strings.

7 Implementation

This assignment requires us to represent places, sets of places, openings, sets of openings, paths, and queues of paths.

We’ll start with the small ones, and work our way up.

You must develop the Place class that contains an x and a y field, and represents a place in the maze. It will need an equals method, just like all of the other classes you develop other than MazeUtils and Tests.

You must develop the Opening class that contains two places, and represents a pathway between two cells. The interesting thing about this class is that since the connections are bidirectional, we would like an opening from place A to place B to be "equals" to an opening from place B to place A. In order to implement this, you’ll need to extend the eclipse-provided "equals" method so that it considers two Openings equal if they contain the same two places, regardless of their order.

To represent sets of places and sets of openings we can use a HashSet. Specifically, we’ll be using HashSet<Place> and HashSet<Opening>. You’ll learn more about what exactly the "Hash" part means later, but for now, you can simply regard these as a high-level way to represent sets.

To represent a path, just use LinkedList<Place>, where the first element in the list represents the end of the path, and the last element represents the beginning of the list (this makes it easier to extend the list.)

To represent a Queue of paths, it turns out that the LinkedList implements the Queue interface. This means that a Queue of Paths will be represented as a LinkedList<LinkedList<Place>>.

Develop the Maze class; it should contain a width in cells, a height in cells, set of openings, and a start and goal Place. Make sure that your constructor accepts these arguments in this order, to make testing possible.

8 Getting Started

If you’re like me, you probably want the satisfaction of seeing something printed before you go on. I would work on the display functions before working on Maze Generation or Maze Solving. Maybe that’s just me, though.

9 Bugs or Corrections

Let me know right away about any bugs or problems in the assignment specification.

10 The Racket Version

#lang racket
 
(require 2htdp/image
           rackunit)
 
; a maze is a set of openings between cells.
; an opening is a set of two cells.
; a cell is a list of two integers.
 
; given the width of the maze in cells, the height in
; cells, and the width and height of a cell, generate
; a maze and a solution and draw them both:
(define (generate-and-draw-maze w h start-location goal-location
                                cell-w cell-h)
 ; generate the maze:
 (define small-maze (fill-up-maze (list start-location)
                                  (set)
                                 (make-empty-board w h)))
 ; find the solution:
 (define solution-path
   (with-handlers ([exn:fail?
                    ; oops, no solution found:
                    (lambda (exn) (eprintf "~a" (exn-message exn))
                      empty)])
     (extend-solution-loop goal-location
                           small-maze
                           (list (list start-location)))))
 ; draw the maze and the solution
 (draw-solution
  w h cell-w cell-h
  solution-path
  (draw-maze w h cell-w cell-h (maze-segments w h small-maze))))
 
; MAZE GENERATION
 
; a partial maze is a set of openings, and a list of "heads"
; of the maze exploration tree, and a map of unused cells
 
; call the "extend" function until there are no
; more heads
(define (fill-up-maze heads openings unused)
 (cond [(empty? heads) openings]
       [else (apply fill-up-maze
                    (extend heads openings unused))]))
 
; choose a head, extend it by one, possibly branching.
; takes the list of path heads, the table of openings,
; and the table of unused cells, and returns a list containing
; the new values for all three.
(define (extend heads openings unused)
 ; pick a head:
 (define shuffled-heads (shuffle heads))
 ; find what's adjacent:
 (define adjacent-empty (adjacent-empties (first shuffled-heads) unused))
 ; how many branches? :
 (define new-heads
   (take (shuffle adjacent-empty)
         (add1 (weighted-random (length adjacent-empty)))))
 ; mark the new head places as used:
 (define new-unused
   (for/fold ([unused unused]) ([head new-heads])
     (set-remove unused head)))
 ; remove the dead ends from the new heads:
 (define non-dead-end-heads
   (filter (lambda (x) (not (empty? (adjacent-empties x new-unused))))
           (append new-heads (rest shuffled-heads))))
 (define new-openings
   (for/fold ([openings openings]) ([head new-heads])
         (set-add openings (set (first shuffled-heads) head))))
 ; return the results
 (list non-dead-end-heads
       new-openings
       new-unused))
 
; return a list of the adjacent cells
(define (adjacent-cells cell)
 (match-define (list x y) cell)
 (list (list (sub1 x) y)
       (list (add1 x) y)
       (list x (sub1 y))
       (list x (add1 y))))
 
; return a list of the adjacent empty cells
(define (adjacent-empties cell unused)
 (filter (lambda (cell) (set-member? unused cell))
         (adjacent-cells cell)))
 
; generate a random number from 0 to n-1 (how many paths
; to follow in extending this path). If the result is greater
; than 0, the path branches.
(define (weighted-random n)
 (cond [(or (= n 1) (< (random) 0.8)) 0]
       [else (add1 (random (sub1 n)))]))
 
 
; make an "unused" map of the given dimensions,
; with 0,0 taken.
(define (make-empty-board w h)
 (set-remove
  (for*/set ([x w] [y h]) (list x y))
  (list 0 0)))
 
; MAZE SOLVING
 
; a path is a list of cells.
; a solve-list is a list of paths.
 
 
; keep extending paths until we get to the end or run out of paths
(define (extend-solution-loop goal-cell openings paths)
 (define (ends-at-goal? path) (equal? (first path) goal-cell))
 ; did we run out of paths to explore?
 (cond [(empty? paths) (error 'extend-solution-loop "no solutions")]
       [else
        (define maybe-found (filter ends-at-goal? paths))
        ; have we not yet found the goal?
        (cond [(empty? maybe-found)
               (define new-paths
                 (extend-solution-path openings (first paths)))
               (extend-solution-loop goal-cell
                                     openings (append
                                               (rest paths)
                                               new-paths))]
              ; we found the goal!
              [else (first maybe-found)])]))
 
; given a map of openings and a path, extend
; in any ways possible to produce a list of paths
(define (extend-solution-path openings path)
 ; what's adjacent to the head of this path?
 (define possible-nexts (adjacent-cells (first path)))
 ; which ones have wall openings to allow passage?
 (define nexts-with-paths
   (filter (lambda (cell) (set-member? openings
                                       (set (first path) cell)))
           possible-nexts))
 ; which ones haven't yet been visited?
 (define non-loop-nexts
   (filter (lambda (cell) (not (member cell path))) nexts-with-paths))
 ; fork the path for each:
 (for/list ([next non-loop-nexts])
   (cons next path)))
 
; GRAPHICS:
 
; given a width and height and openings,
; return a list of all of the segments
; to be drawn in the list.
(define (maze-segments w h openings)
 (apply
  append
  (for*/list ([x (in-range w)]
              [y (in-range h)])
    (append
     (cond [(and
             (< x (sub1 w))
             (not
              (set-member? openings (set (list x y) (list (add1 x) y)))))
            (list (list (add1 x) y (add1 x) (add1 y)))]
           [else empty])
     (cond [(and
             (< y (sub1 h))
             (not
              (set-member? openings (set (list x y) (list x (add1 y))))))
            (list (list x (add1 y) (add1 x) (add1 y)))]
           [else empty])))))
 
 
; given a width, a height, a cell-width and cell-height,
; and a list of segments (in cell coordinates), return
; a scene containing the maze.
(define (draw-maze w h cell-w cell-h segments)
 (foldr
  (lambda (segment scene)
    (add-line scene
              (* cell-w (first segment))
              (* cell-h (second segment))
              (* cell-w (third segment))
              (* cell-h (fourth segment))
              "black"))
  (empty-scene (* w cell-w) (* h cell-h))
  segments))
 
; make the blue square image used to indicate a path
(define (make-path-square-image cell-w cell-h)
 (rectangle (ceiling (/ cell-w 2))
            (ceiling  (/ cell-h 2))
            "solid"
            "blue"))
 
; given the maze parameters and the path and the image, draw
; blue dots along the path
(define (draw-solution w h cell-w cell-h path maze-image)
 (define path-square-image (make-path-square-image cell-w cell-h))
 (for/fold ([image maze-image]) ([cell path])
   (underlay/xy image
                (* (+ 1/4 (first cell)) cell-w)
                (* (+ 1/4 (second cell)) cell-h)
                path-square-image)))
 
 
 
 
(generate-and-draw-maze 60 60 (list 0 0) (list 59 59) 8 8)
(generate-and-draw-maze 4 4 (list 1 0) (list 3 3) 20 20)
 
; TEST CASES
 
(check-equal? (maze-segments 1 1 (set)) empty)
(check-equal? (list->set (maze-segments 3 2 (set)))
              (set (list 1 0 1 1)
                   (list 1 1 1 2)
                   (list 2 0 2 1)
                   (list 2 1 2 2)
                   (list 0 1 1 1)
                   (list 1 1 2 1)
                   (list 2 1 3 1)))
(check-equal? (list->set (maze-segments 3 2
                                        (set (set (list 1 1)
                                                  (list 1 0))
                                            (set (list 1 1)
                                                 (list 2 1)))))
             (set (list 1 0 1 1)
                  (list 1 1 1 2)
                  (list 2 0 2 1)
                  (list 0 1 1 1)
                  (list 2 1 3 1)))
 
(define three-cells-taken (set (list 0 0) (list 0 1) (list 1 1)))
(check-equal? (length (adjacent-empties (list 2 0) three-cells-taken)) 0)
(check-equal? (length (adjacent-empties (list 1 1) three-cells-taken)) 1)
(check-equal? (length (adjacent-empties (list 0 1) three-cells-taken)) 2)
 
 
 
(check-equal? (extend-solution-path (set (set (list 3 3) (list 3 2))
                                         (set (list 3 3) (list 4 3)))
                          (list (list 3 3) (list 3 2) (list 3 1)))
             (list (list (list 4 3) (list 3 3) (list 3 2) (list 3 1))))
 
(define 3x3-empty (make-empty-board 3 3))
(define 5x5-empty (make-empty-board 5 5))
 
(check-equal? (adjacent-empties (list 0 0) 5x5-empty)
              (list (list 1 0) (list 0 1)))