diff --git a/projekt/Labyrinth.java b/projekt/Labyrinth.java new file mode 100644 index 0000000..d56feb2 --- /dev/null +++ b/projekt/Labyrinth.java @@ -0,0 +1,260 @@ +/* + * A labyrinth generated using the depth-first algorithm + * (www.astrolog.org/labyrnth/algrithm.htm), with a start point and end point + * for a search and with a display (unless too large) as ASCII graphics and + * Swing graphics. + * Source of labyrinth representation and ASCII output generation: + * http://rosettacode.org/wiki/Maze#Java + */ + + + +import java.awt.Color; +import java.awt.Graphics; +import java.io.Serializable; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collections; + +public final class Labyrinth implements Serializable{ + private static final long serialVersionUID = 1L; + + /** + * Serialized state of a labyrinth with size, passages, start and end + * (without search state) and with all information defining its graphic + * and textual display. + */ + + private final int width; // total number of cells in x direction + private final int height; // total number of cells in y direction + private final Point start; // starting point of the search + private final Point end; // end point of the search + + private final byte[][] passages; + /* + * Each array element represents a cell in the labyrinth with the passages possible from + * this cell. Its four least significant bits are interpreted as one flag for each direction + * (see enum Direction for which bit means which direction) indicating whether + * there is a passage from this cell in that direction (note that passages + * and walls are not cells, but represented indirectly by these flags). + * Initially all cells are 0, i.e. have no passage from them (i.e. surrounded + * by walls on all their four sides). Note that two-way passages appear as opposite + * bits in both the source and destination cell; thus, this data structure supports + * one-way passages, too, by setting a bit in the source cell only (however, one-way + * passages are not used in PAR). + */ + + // When generating the labyrinth and considering whether to create a passage to some neighbor cell, create a + // passage to a cell that is already accessible on another path (i.e. create a cycle) with this probability: + private static final double CYCLE_CREATION_PROBABILITY = 0.01; + + private static final int CELL_PX = 10; // width and length of the labyrinth cells in pixels + private static final int HALF_WALL_PX = 2; // thickness/2 of the labyrinth walls in pixels + // labyrinths with more pixels than this (in one or both directions) will not be graphically displayed: + private static final int MAX_PX_TO_DISPLAY = 1000; + + public Labyrinth(int width, int height) { + this.width = width; + this.height = height; + + // Always start in the center of the labyrinth: + start = new Point(width/2, height/2); + + // Randomly pick a cell on the boundary as the end point: + int endIndex = (int)((2*width + 2*height) * Math.random()); + int endX; + int endY; + // Try the four edges of the grid, starting at the upper edge, + // proceeding clockwise to the left edge: + if (endIndex < width) { // upper edge + endX = endIndex; + endY = 0; + } else { + if (endIndex < width + height) { // right edge + endX = width-1; + endY = endIndex - width; + } else { + if (endIndex < 2*width + height) { // lower edge + endX = endIndex - width - height; + endY = height-1; + } else { // left edge + endX = 0; + endY = endIndex - 2*width - height; + } + } + } + end = new Point(endX, endY); + + passages = new byte[width][height]; // initially all 0 (see comment at declaration of passages) + makePassages(); + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public Point getStart() { + return start; + } + + public boolean hasPassage(Point from, Direction directionToNeighbor) { + return contains(from) && (passages[from.getX()][from.getY()] & directionToNeighbor.bit) != 0; + } + + public boolean hasPassage(Point from, Point to) { + if (!contains(from) || !contains(to)) { + return false; + } + if (from.getNeighbor(Direction.N).equals(to)) + return (passages[from.getX()][from.getY()] & Direction.N.bit) != 0; + if (from.getNeighbor(Direction.S).equals(to)) + return (passages[from.getX()][from.getY()] & Direction.S.bit) != 0; + if (from.getNeighbor(Direction.E).equals(to)) + return (passages[from.getX()][from.getY()] & Direction.E.bit) != 0; + if (from.getNeighbor(Direction.W).equals(to)) + return (passages[from.getX()][from.getY()] & Direction.W.bit) != 0; + return false; // To suppress warning about undefined return value + } + + public boolean contains(Point p) { + return 0 <= p.getX() && p.getX() < width && + 0 <= p.getY() && p.getY() < height; + } + + public boolean isDestination(Point p) { + return p.equals(end); + } + + /** + * Return whether p, when coming from fromDir, is a blind alley. + */ + public boolean isBlindAlley(Point p, Direction fromDir) { + int directionBitsExceptFromDir = Direction.allDirectionBits & ~fromDir.bit; + return (passages[p.getX()][p.getY()] & directionBitsExceptFromDir) == 0; + } + + /** + * Generate a labyrinth (with or without cycles, depending on CYCLE_CREATION_PROBABILITY) + * using the depth-first algorithm (www.astrolog.org/labyrnth/algrithm.htm (sic!)) + */ + + private void makePassages() { + ArrayDeque pointsToDo = new ArrayDeque(); + Point current; + pointsToDo.push(getStart()); + while (!pointsToDo.isEmpty()) { + current = pointsToDo.pop(); + int cx = current.getX(); + int cy = current.getY(); + Direction[] dirs = Direction.values(); + Collections.shuffle(Arrays.asList(dirs)); + // For all unvisited neighboring cells in random order: + // Make a passage from the current cell to that neighbor + for (Direction dir : dirs) { + // Pick random neighbor of current cell as new cell (nx, ny) + Point neighbor = current.getNeighbor(dir); + int nx = neighbor.getX(); + int ny = neighbor.getY(); + + if (contains(neighbor) // If neighbor is still in the labyrinth ... + && ( passages[nx][ny] == 0 // ... and has no passage yet, i.e. has not been visited yet during generation + || Math.random() < CYCLE_CREATION_PROBABILITY )) { // ... or creating a cycle is OK + + // Make a two-way passage, i.e. from current to neighbor and from neighbor to current: + passages[cx][cy] |= dir.bit; + passages[nx][ny] |= dir.opposite.bit; + + // Remember to continue from this neighbor later on + pointsToDo.push(neighbor); + } + } + } + } + + + public void print() { + System.out.println("Labyrinth with start " + start + " and end " + end); + for (int i = 0; i < height; i++) { + // draw the north edges + for (int j = 0; j < width; j++) { + System.out.print((passages[j][i] & Direction.N.bit) == 0 ? "+---" : "+ "); + } + System.out.println("+"); + // draw the west edges + for (int j = 0; j < width; j++) { + System.out.print((passages[j][i] & Direction.W.bit) == 0 ? "| " : " "); + } + // draw the far east edge + System.out.println("|"); + } + // draw the bottom line + for (int j = 0; j < width; j++) { + System.out.print("+---"); + } + System.out.println("+"); + } + + public int cell_size_pixels() { + return CELL_PX; + } + + public boolean smallEnoughToDisplay() { + return width*CELL_PX <= MAX_PX_TO_DISPLAY && height*CELL_PX <= MAX_PX_TO_DISPLAY; + } + + public void display(Graphics graphics) { + // draw start and end cell in special colors (covering start and end cell of the solution path) + graphics.setColor(Color.RED); + graphics.fillRect(start.getX()*CELL_PX, start.getY()*CELL_PX, CELL_PX, CELL_PX); + graphics.setColor(Color.GREEN); + graphics.fillRect(end.getX()*CELL_PX, end.getY()*CELL_PX, CELL_PX, CELL_PX); + + // draw black walls (covering part of the solution path) + graphics.setColor(Color.BLACK); + for(int x = 0; x < width; ++x) { + for(int y = 0; y < height; ++y) { + // draw north edge of each cell (together with south edge of cell above) + if ((passages[x][y] & Direction.N.bit) == 0) + // y-HALF_WALL_PX will be half out of labyrinth for x==0 row, + // but that does not hurt the picture thanks to automatic cropping + graphics.fillRect(x*CELL_PX, y*CELL_PX-HALF_WALL_PX, CELL_PX, 2*HALF_WALL_PX); + // draw west edge of each cell (together with east edge of cell to the left) + if ((passages[x][y] & Direction.W.bit) == 0) + // x-HALF_WALL_PX will be half out of labyrinth for y==0 column, + // but that does not hurt the picture thanks to automatic cropping + graphics.fillRect(x*CELL_PX-HALF_WALL_PX, y*CELL_PX, 2*HALF_WALL_PX, CELL_PX); + } + } + // draw east edge of labyrinth + graphics.fillRect(width*CELL_PX, 0, HALF_WALL_PX, height*CELL_PX); + // draw south edge of labyrinth + graphics.fillRect(0, height*CELL_PX-HALF_WALL_PX, width*CELL_PX, HALF_WALL_PX); + } + + + public boolean checkSolution(Point solution[]) { + Point from = solution[0]; + if (!from.equals(start)) { + System.out.println("checkSolution fails because the first cell is" + from + ", but not " + start); + return false; + } + + for (int i = 1; i < solution.length; ++i) { + Point to = solution[i]; + if (!hasPassage(from, to)) { + System.out.println("checkSolution fails because there is no passage from " + from + " to " + to); + return false; + } + from = to; + } + if (!from.equals(end)) { + System.out.println("checkSolution fails because the last cell is" + from + ", but not " + end); + return false; + } + return true; + } +} diff --git a/projekt/Point.java b/projekt/Point.java new file mode 100644 index 0000000..d9a0cf8 --- /dev/null +++ b/projekt/Point.java @@ -0,0 +1,46 @@ +/* + * An immutable class for a 2D point that can safely be shared among threads. + * + * Author: Holger.Peine@hs-hannover.de + * + */ + + + +import java.io.Serializable; + +public final class Point implements Serializable { + private static final long serialVersionUID = 1L; + final int x, y; + Point(int x, int y) { + this.x = x; + this.y = y; + } + + int getX() { return x; } + int getY() { return y; } + + final Point getNeighbor(Direction dir) { + return new Point(x+dir.dx, y+dir.dy); + } + + @Override + public String toString() { + return "("+x+", "+y+")"; + } + + @Override + public boolean equals(Object other) { + if (other == this) + return true; + if (other.getClass() != this.getClass()) + return false; + Point p = (Point)other; + return x == p.x && y == p.y; + } + + @Override + public int hashCode() { + return 3001*x+y; // 3001 is prime + } +} diff --git a/projekt/PointAndDirection.java b/projekt/PointAndDirection.java new file mode 100644 index 0000000..30e1c45 --- /dev/null +++ b/projekt/PointAndDirection.java @@ -0,0 +1,59 @@ +/* + * An enum for 2D directions (north, west, south, east) represented as a bit vector, + * and an immutable class that packages a Point with such a direction. + * + * Author: Holger.Peine@hs-hannover.de + * Source of enum Direction: http://rosettacode.org/wiki/Maze#Java + */ + + + +enum Direction { + N(1, 0, -1), S(2, 0, 1), E(4, 1, 0), W(8, -1, 0); + static final int allDirectionBits = N.bit | S.bit | E.bit | W.bit; + final int bit; + final int dx; + final int dy; + Direction opposite; + + // use the static initializer to resolve forward references + static { + N.opposite = S; + S.opposite = N; + E.opposite = W; + W.opposite = E; + } + + private Direction(int bit, int dx, int dy) { + this.bit = bit; + this.dx = dx; + this.dy = dy; + } + + @Override + public String toString() { + switch(this) { + case N: return "N"; + case S: return "S"; + case W: return "W"; + case E: return "E"; + default: return "?"; + } + } +} + +final class PointAndDirection { + final private Point point; + public Point getPoint() { + return point; + } + final private Direction directionToBranchingPoint; + public Direction getDirectionToBranchingPoint() { + return directionToBranchingPoint; + } + PointAndDirection(Point p, Direction direction) { + this.point = p; + directionToBranchingPoint = direction; + } +} + diff --git a/projekt/Solver.java b/projekt/Solver.java new file mode 100644 index 0000000..b3eb119 --- /dev/null +++ b/projekt/Solver.java @@ -0,0 +1,270 @@ +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.GridLayout; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import java.util.ArrayDeque; +import java.util.Arrays; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; + +final public class Solver extends JPanel { + + private static final long serialVersionUID = 1L; + + // The default size of the labyrinth (i.e. unless program is invoked with size arguments): + private static final int DEFAULT_WIDTH_IN_CELLS = 100; + private static final int DEFAULT_HEIGHT_IN_CELLS = 100; + + private static final int N_RUNS_HALF = 5; // #runs will be 2*N_RUNS_HALF + 1 + + // The grid defining the structure of the labyrinth + private final Labyrinth labyrinth; + + // For each cell in the labyrinth: Has solve() visited it yet? + private boolean[][] visited; // initialized in solve() + + private Point[] solution = null; // set to solution path once that has been computed + + public Solver(Labyrinth labyrinth) { + this.labyrinth = labyrinth; + } + + public Solver(int width, int height) { + this(new Labyrinth(width, height)); + } + + private boolean visitedBefore(Point p) { + return visited[p.getX()][p.getY()]; + } + + private void visit(Point p) { + visited[p.getX()][p.getY()] = true; + } + + /** + * @return Returns a path through the labyrinth from start to end as an array, or null if no solution exists + */ + public Point[] solve() { + + // Initialize the search state: This must be done here to be part of the timing measurement + + Point current = labyrinth.getStart(); + ArrayDeque pathSoFar = new ArrayDeque(); // Path from start to just before current + visited = new boolean[labyrinth.getWidth()][labyrinth.getHeight()]; // initially all false + ArrayDeque backtrackStack = new ArrayDeque(); + // Used as a stack: Branches not yet taken; solver will backtrack to these branching points later + // TODO: Is it faster to allocate backtrackStack with width*height elements right away? + + // Search: + + while (!labyrinth.isDestination(current)) { + Point next = null; + visit(current); + + // Use first random unvisited neighbor as next cell, push others on the backtrack stack: + Direction[] dirs = Direction.values(); + for (Direction directionToNeighbor: dirs) { + Point neighbor = current.getNeighbor(directionToNeighbor); + if ( labyrinth.hasPassage(current, directionToNeighbor) + && !visitedBefore(neighbor) + && ( !labyrinth.isBlindAlley(neighbor, directionToNeighbor.opposite) + || labyrinth.isDestination(neighbor))) { + if (next == null) // 1st unvisited neighbor + next = neighbor; + else { + // 2nd or higher unvisited neighbor: Save neighbor as starting cell for a later backtracking + backtrackStack.push(new PointAndDirection(neighbor, directionToNeighbor.opposite)); + // System.out.println("Pushing " + neighbor + " to the backtracking stack."); + } + } + } + // Advance to next cell, if any: + if (next != null) { + // System.out.println("Advancing from " + current + " to " + next); + pathSoFar.addLast(current); + current = next; + } else { + // current has no unvisited neighbor: Backtrack, if possible + if (backtrackStack.isEmpty()) + return null; // No more backtracking avaible: No solution exists + + // Backtrack: Continue with cell saved at latest branching point: + PointAndDirection pd = backtrackStack.pop(); + current = pd.getPoint(); + Point branchingPoint = current.getNeighbor(pd.getDirectionToBranchingPoint()); + // System.out.println("Backtracking to " + branchingPoint); + // Remove the dead end from the top of pathSoFar, i.e. all cells after branchingPoint: + while (!pathSoFar.peekLast().equals(branchingPoint)) { + // System.out.println(" Going back before " + pathSoFar.peekLast()); + pathSoFar.removeLast(); + } + } + } + pathSoFar.addLast(current); + // Point[0] is only for making the return value have type Point[] (and not Object[]): + return pathSoFar.toArray(new Point[0]); + } + + @Override + protected void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + // draw white background + graphics.setColor(Color.WHITE); + graphics.fillRect(0, 0, labyrinth.getWidth()*labyrinth.cell_size_pixels(), labyrinth.getHeight()*labyrinth.cell_size_pixels()); + + // draw solution path, if available + if (solution != null) { + graphics.setColor(Color.YELLOW); + for (Point p: solution) +/* // fill only white area between the walls instead of whole cell: + graphics.fillRect(p.getX()*CELL_PX+HALF_WALL_PX, p.getY()*CELL_PX+HALF_WALL_PX, + CELL_PX-2*HALF_WALL_PX, CELL_PX-2*HALF_WALL_PX); +*/ + graphics.fillRect(p.getX()*labyrinth.cell_size_pixels(), p.getY()*labyrinth.cell_size_pixels(), + labyrinth.cell_size_pixels(), labyrinth.cell_size_pixels()); + } + // draw walls + labyrinth.display(graphics); + } + + public void printSolution() { + System.out.print("Solution: "); + for (Point p: solution) + System.out.print(p); + System.out.println(); + } + + public void displaySolution() { + repaint(); +} + +private static Solver makeAndSaveSolver(String[] args) { + + // Construct solver: Either read it from a file, or create a new one + if (args.length >= 1 && args[0].endsWith(".ser")) { + + // 1st argument is name of file with serialized labyrinth: Ignore other arguments + // and create a solver for the labyrinth from that file: + ObjectInputStream ois; + try { + ois = new ObjectInputStream(new FileInputStream(args[0])); + Labyrinth labyrinth = (Labyrinth)ois.readObject(); + ois.close(); + return new Solver(labyrinth); + } catch (Exception e) { + System.out.println(e); + return null; + } + } else { + // Create solver for new, random labyrinth: + + int width = args.length >= 1 ? (Integer.parseInt(args[0])) : DEFAULT_WIDTH_IN_CELLS; + int height = args.length >= 2 ? (Integer.parseInt(args[1])) : DEFAULT_HEIGHT_IN_CELLS; + + Solver solver = new Solver(width, height); + + // Save labyrinth to file (may be reused in future program executions): + try { + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("labyrinth.ser")); + oos.writeObject(solver.labyrinth); + oos.close(); + } catch (Exception e) { + System.out.println(e); + } + + return solver; + } +} + + +private static void displayLabyrinth(Solver solver) { + JFrame frame = new JFrame("Sequential labyrinth solver"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + // TODO: Window is initially displayed somewhat smaller than + // the indicated frame size, therefore use width+5 and height+5: + frame.setSize((solver.labyrinth.getWidth()+5) * solver.labyrinth.cell_size_pixels(), + (solver.labyrinth.getHeight()+5) * solver.labyrinth.cell_size_pixels()); + + // Put a scroll pane around the labyrinth frame if the latter is too large + // (by Joern Lenselink) + Dimension displayDimens = java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds().getSize(); + Dimension labyrinthDimens = frame.getSize(); + if(labyrinthDimens.height > displayDimens.height) { + JScrollPane scroll = new JScrollPane(); + solver.setBackground(Color.LIGHT_GRAY); + frame.getContentPane().add(scroll); + JPanel borderlayoutpanel = new JPanel(); + borderlayoutpanel.setBackground(Color.darkGray); + scroll.setViewportView(borderlayoutpanel); + borderlayoutpanel.setLayout(new BorderLayout(0, 0)); + + JPanel columnpanel = new JPanel(); + borderlayoutpanel.add(columnpanel, BorderLayout.NORTH); + columnpanel.setLayout(new GridLayout(0, 1, 0, 1)); + columnpanel.setOpaque(false); + columnpanel.setBackground(Color.darkGray); + + columnpanel.setSize(labyrinthDimens.getSize()); + columnpanel.setPreferredSize(labyrinthDimens.getSize()); + columnpanel.add(solver); + } else { + // No scroll pane needed: + frame.getContentPane().add(solver); + } + + frame.setVisible(true); // will draw the labyrinth (without solution) +} + +/** + * + * @param args If the first argument is a file name ending in .ser, the serialized labyrinth in that file + * is used; else the first two arguments are optional numbers giving the width and height of a new + * labyrinth to be constructed. Then the labyrinth is solved and displayed (unless too large). + * This is run a certain number of times and then the median run time is printed. + */ + public static void main(String[] args) { + long[] runTimes = new long[2*N_RUNS_HALF + 1]; + + for (int run = 0; run < 2*N_RUNS_HALF + 1; ++run) { + + Solver solver = makeAndSaveSolver(args); + if (solver.labyrinth.smallEnoughToDisplay()) { + displayLabyrinth(solver); + } + + long startTime = System.currentTimeMillis(); + solver.solution = solver.solve(); + long endTime = System.currentTimeMillis(); + + if (solver.solution == null) + System.out.println("No solution exists."); + else { + System.out.println("Computed sequential solution of length " + solver.solution.length + " to labyrinth of size " + + solver.labyrinth.getWidth() + "x" + solver.labyrinth.getHeight() + " in " + (endTime - startTime) + "ms."); + + runTimes[run] = endTime - startTime; + + if (solver.labyrinth.smallEnoughToDisplay()) { + solver.displaySolution(); + solver.printSolution(); + } + + if (solver.labyrinth.checkSolution(solver.solution)) + System.out.println("Solution correct :-)"); + else + System.out.println("Solution incorrect :-("); + } + } + Arrays.sort(runTimes); + System.out.println("Median run time was " + runTimes[N_RUNS_HALF] + " ms."); + } +} \ No newline at end of file