Rekursive Labyrinthe/ MazeGraphs
Erscheinungsbild
/*
Source file: MazeGraphs.java
Program: MazeGraphs
Author: Markus Bautsch
Licence: public domain
Date: 3 October 2021
Version: 1.1
Programming language: Java
*/
/*
This class implements a graphical maze as an extension of the class javax.swing.JFrame.
A maze will be built during initalisation by a recursive algorithm.
The coordinates of the upper left maze field are (0 | 0).
This class implements the drawing of a maze.
The graphical design of walls and borders are determined by this class.
*/
public class MazeGraphs extends javax.swing.JFrame
{
/* serialVersionUID for serialisation implemented in class javax.swing.JFrame */
private static final long serialVersionUID = 1L;
/* Constant for defining maximum size of the maze */
public static final int MAZE_MAX_SIZE = 128;
/* Constants for constructing mazes that can be displayed or saved to a file */
public static final boolean Display = true;
public static final boolean DontDisplay = false;
public static final boolean Save = true;
public static final boolean DontSave = false;
/* Size of rectangular maze grid in fields with default initialisation */
private int MAZE_GRID_WIDTH = 16;
private int MAZE_GRID_HEIGHT = 16;
/* Start and end field of maze and path length counters */
private MazeField startField = null;
private MazeField endField = null;
private static long currentLengthOfPath = 0;
private static long lengthOfPathToEndField = 0;
/* Constants for defining size and look of maze fields */
private static final int FIELD_WIDTH = 24;
private static final int FIELD_HEIGHT = 24;
/* Variables for top banner height and left offset width */
private static final int offset = 15;
private static final int yBannerHeight = 31;
/* Instance variables */
private boolean display; /* is true, if maze shall be display on the screen */
private boolean save; /* is true, if maze shall be saved to a PNG-file */
/* The constructor will initialise a mazeArray with width and height */
private MazeArray mazeArray;
/* Variable for choosing random directions initialised with current system time */
private static java.util.Random randomdirection = new java.util.Random (java.lang.System.currentTimeMillis ());
/* Constructor for object instances of class MazeGraphs */
public MazeGraphs (int width, int height, boolean displayMaze, boolean saveMaze) throws MazeException
{
super ("Maze " + width + " x " + height);
this.setDefaultCloseOperation (javax.swing.WindowConstants.EXIT_ON_CLOSE);
if ((width > 1) && (width <= MAZE_MAX_SIZE))
{
MAZE_GRID_WIDTH = width;
}
else
{
throw new MazeException ("Maze cannot be created with width " + width);
}
if ((height > 1) && (height <= MAZE_MAX_SIZE))
{
MAZE_GRID_HEIGHT = height;
}
else
{
throw new MazeException ("Maze cannot be created with height " + height);
}
mazeArray = new MazeArray (MAZE_GRID_WIDTH, MAZE_GRID_HEIGHT);
this.display = displayMaze;
this.save = saveMaze;
}
private static int getRectWidth (int gridWidth)
{
int rectWidth = (2 * offset) + (FIELD_WIDTH * gridWidth);
return rectWidth;
}
private static int getRectHeight (int gridHeight)
{
int rectHeight = yBannerHeight + (2 * offset) + (FIELD_HEIGHT * gridHeight);
return rectHeight;
}
private static int getLeftEdge (int x)
{
int leftEdge = offset + (x * FIELD_WIDTH);
return leftEdge;
}
private static int getUpperEdge (int y)
{
int upperEdge = yBannerHeight + offset + (y * FIELD_HEIGHT);
return upperEdge;
}
/* Draw single maze field */
private static void drawMazeField (java.awt.Graphics graphics, MazeField mazeField)
{
int x = mazeField.getLocationX ();
int y = mazeField.getLocationY ();
int leftEdge = getLeftEdge (x);
int rightEdge = leftEdge + (FIELD_WIDTH - 1);
int upperEdge = getUpperEdge (y);
int lowerEdge = upperEdge + (FIELD_HEIGHT - 1);
graphics.setColor (java.awt.Color.BLUE);
if (mazeField.hasWallN ())
{
graphics.drawLine (leftEdge, upperEdge, rightEdge, upperEdge);
}
if (mazeField.hasWallE ())
{
graphics.drawLine (rightEdge, upperEdge, rightEdge, lowerEdge);
}
if (mazeField.hasWallS ())
{
graphics.drawLine (leftEdge, lowerEdge, rightEdge, lowerEdge);
}
if (mazeField.hasWallW ())
{
graphics.drawLine (leftEdge, upperEdge, leftEdge, lowerEdge);
}
if (mazeField.hasAllBorders ())
{
graphics.setColor (java.awt.Color.lightGray);
graphics.fillRect (leftEdge + 1, upperEdge + 1, FIELD_WIDTH - 2, FIELD_HEIGHT - 2);
}
}
/* Draw mark in maze field */
private static void drawMarkInField (java.awt.Graphics graphics, MazeField mazeField, java.awt.Color colour)
{
final int width = FIELD_WIDTH / 2;
final int offsetW = (FIELD_WIDTH - width) / 2;
final int height = FIELD_HEIGHT / 2;
final int offsetH = (FIELD_HEIGHT - height) / 2;
int x = mazeField.getLocationX ();
int y = mazeField.getLocationY ();
int left = getLeftEdge (x) + offsetW;
int upper = getUpperEdge (y) + offsetH;
graphics.setColor (colour);
graphics.fillRect (left, upper, width, height);
}
/* Draw complete maze */
private static void drawMaze (java.awt.Graphics graphics, MazeArray mazeArray)
{
int width = mazeArray.getWidth ();
int height = mazeArray.getHeight ();
/* draw rectangular background frame for maze */
graphics.setColor (java.awt.Color.white);
int rectWidth = getRectWidth (width);
int rectHeight = getRectHeight (height);
graphics.fillRect (0, 0, rectWidth, rectHeight);
/* draw maze fields */
int y = 0;
while (y < height)
{
int x = 0;
while (x < width)
{
drawMazeField (graphics, mazeArray.fields [x][y]);
x++;
}
y++;
}
}
/* Method save for saving a maze to a graphical PNG file */
private void saveFile (java.awt.image.BufferedImage bufferedImage)
{
final java.lang.String fileExtension = "png";
java.util.Iterator <javax.imageio.ImageWriter> iter = javax.imageio.ImageIO.getImageWritersByFormatName (fileExtension);
javax.imageio.ImageWriter imageWriter = (javax.imageio.ImageWriter) iter.next ();
javax.imageio.ImageWriteParam imageWriteParam = imageWriter.getDefaultWriteParam ();
imageWriteParam.setCompressionMode (javax.imageio.ImageWriteParam.MODE_DISABLED);
try
{
java.lang.String fileName = "Maze_" + this.MAZE_GRID_WIDTH + "x" + this.MAZE_GRID_HEIGHT + "." + fileExtension;
javax.imageio.stream.FileImageOutputStream fileImageOutputStream = new javax.imageio.stream.FileImageOutputStream (new java.io.File (fileName));
imageWriter.setOutput (fileImageOutputStream);
javax.imageio.IIOImage iIOimg = new javax.imageio.IIOImage ((java.awt.image.RenderedImage) bufferedImage, null, null);
java.lang.System.out.println ("Save " + fileName);
imageWriter.write (null, iIOimg, imageWriteParam);
}
catch (java.lang.Exception exception)
{
exception.printStackTrace ();
}
}
/* Method init for initialisation of maze */
private void init ()
{
// Set frame size and background
int rectWidth = getRectWidth (MAZE_GRID_WIDTH);
int rectHeight = getRectHeight (MAZE_GRID_HEIGHT);
this.setSize (rectWidth, rectHeight);
this.setBackground (java.awt.Color.WHITE);
java.awt.image.BufferedImage bufferedImage = new java.awt.image.BufferedImage (rectWidth, rectHeight, java.awt.image.BufferedImage.TYPE_INT_RGB);
java.awt.Graphics2D graphics2D = bufferedImage.createGraphics ();
this.paint (graphics2D);
if (this.save)
{
this.saveFile (bufferedImage);
}
this.setVisible (this.display);
}
/* This method sets all borders of a maze field and therefore excludes it from maze path */
public void excludeMazeField (int x, int y)
{
int width = mazeArray.getWidth ();
int height = mazeArray.getHeight ();
if ((x >= 0) && (x < width) && (y >= 0) && (y < height))
{
mazeArray.fields [x][y].setAllBorders ();
if (y > 0)
{
mazeArray.fields [x][y - 1].setBorderS ();
}
if (x < (width - 1))
{
mazeArray.fields [x + 1][y].setBorderW ();
}
if (y < (height - 1))
{
mazeArray.fields [x][y + 1].setBorderN ();
}
if (x > 0)
{
mazeArray.fields [x - 1][y].setBorderE ();
}
}
}
/* Remove walls between current field and previous field */
private void removeWalls (MazeField currentField)
{
MazeField previousField = currentField.getPreviousField ();
if ((currentField != null) && (previousField != null))
{
if (currentField.getLocationX () == previousField.getLocationX ())
{
if (currentField.getLocationY () > previousField.getLocationY ())
{
currentField.removeWallN ();
previousField.removeWallS ();
}
if (currentField.getLocationY () < previousField.getLocationY ())
{
currentField.removeWallS ();
previousField.removeWallN ();
}
}
else
{
if (currentField.getLocationY () == previousField.getLocationY ())
{
if (currentField.getLocationX () > previousField.getLocationX ())
{
currentField.removeWallW ();
previousField.removeWallE ();
}
if (currentField.getLocationX () < previousField.getLocationX ())
{
currentField.removeWallE ();
previousField.removeWallW ();
}
}
}
}
}
/* Check possible neighbour field to the left (west) and return it if possible */
private MazeField checkNeighbourW (MazeField field)
{
int x = field.getLocationX();
int y = field.getLocationY();
MazeField possibleNeighbour = null;
if (x > 0)
{
MazeField neighbourField = mazeArray.fields [x - 1][y];
if (neighbourField.hasAllWalls () && !field.hasBorderW () && !neighbourField.hasBorderE ())
{
possibleNeighbour = neighbourField;
}
}
return possibleNeighbour;
}
/* Check possible neighbour field to the right (east) and return it if possible */
private MazeField checkNeighbourE (MazeField field)
{
int x = field.getLocationX();
int y = field.getLocationY();
MazeField possibleNeighbour = null;
int maxX = mazeArray.getWidth () - 1;
if (x < maxX)
{
MazeField neighbourField = mazeArray.fields [x + 1][y];
if (neighbourField.hasAllWalls () && !field.hasBorderE () && !neighbourField.hasBorderW ())
{
possibleNeighbour = neighbourField;
}
}
return possibleNeighbour;
}
/* Check possible neighbour field to the top (north) and return it if possible */
private MazeField checkNeighbourN (MazeField field)
{
int x = field.getLocationX();
int y = field.getLocationY();
MazeField possibleNeighbour = null;
if (y > 0)
{
MazeField neighbourField = mazeArray.fields [x][y - 1];
if (neighbourField.hasAllWalls () && !field.hasBorderN () && !neighbourField.hasBorderS ())
{
possibleNeighbour = neighbourField;
}
}
return possibleNeighbour;
}
/* Check possible neighbour field to the bottom (south) and return it if possible */
private MazeField checkNeighbourS (MazeField field)
{
int x = field.getLocationX();
int y = field.getLocationY();
MazeField possibleNeighbour = null;
int maxY = mazeArray.getHeight () - 1;
if (y < maxY)
{
MazeField neighbourField = mazeArray.fields [x][y + 1];
if (neighbourField.hasAllWalls () && !field.hasBorderS () && !neighbourField.hasBorderN ())
{
possibleNeighbour = neighbourField;
}
}
return possibleNeighbour;
}
/* Compute and return random direction from 1 to MazeField.NumberOfDirections */
private static byte getRandomDirection ()
{
byte random = (byte) (randomdirection.nextLong () % MazeField.NumberOfDirections + 1);
return random;
}
/* This method checks whether the walls between two random maze fields next to each other can be removed.
* If possible then an appropriate neighbour else null will be returned. */
private MazeField checkNeighbours (MazeField field)
{
MazeField neighbour = null;
boolean triedN = false; // for direction north
boolean triedE = false; // for direction east
boolean triedS = false; // for direction south
boolean triedW = false; // for direction west
while ((neighbour == null) && (!triedN || !triedE || !triedS || !triedW))
{
byte direction = getRandomDirection ();
if (!triedN && (direction == MazeField.NORTH))
{
neighbour = checkNeighbourN (field);
triedN = true;
}
else if (!triedE && (direction == MazeField.EAST))
{
neighbour = checkNeighbourE (field);
triedE = true;
}
else if (!triedS && (direction == MazeField.SOUTH))
{
neighbour = checkNeighbourS (field);
triedS = true;
}
else if (!triedW && (direction == MazeField.WEST))
{
neighbour = checkNeighbourW (field);
triedW = true;
}
}
return neighbour;
}
/* This method sets all outer borders */
private void setBorders ()
{
int width = mazeArray.getWidth ();
int height = mazeArray.getHeight ();
int y = 0;
while (y < height)
{
mazeArray.fields [0][y].setBorderW ();
mazeArray.fields [width - 1][y].setBorderE ();
int x = 0;
while (x < width)
{
mazeArray.fields [x][0].setBorderN ();
mazeArray.fields [x][height - 1].setBorderS ();
x++;
}
y++;
}
}
/* This method searches for the first maze field without four borders,
* which can be used as the first current field.
*/
private MazeField findFirstField () throws MazeException
{
MazeField currentField = null;
int width = mazeArray.getWidth ();
int height = mazeArray.getHeight ();
int y = 0;
while ((currentField == null) && (y < height))
{
int x = 0;
while ((currentField == null) && (x < width))
{
if (!mazeArray.fields [x][y].hasAllBorders ())
{
currentField = mazeArray.fields [x][y];
}
x++;
}
y++;
}
if (currentField == null)
{
throw new MazeException ("start field cannot be found!");
}
return currentField;
}
/* If a neighbour of the current maze field has four walls
* it will become open for a maze path as the new current field.
* The new maze field will become current field,
* the walls between the two fields will be removed
* and the method will call itself recursively until all maze fields are processed.
*/
private void findNeighbours (MazeField field)
{
if (field != null)
{
MazeField neighbour = checkNeighbours (field);
if (neighbour != null)
{
neighbour.setPreviousField (field);
field = neighbour;
removeWalls (field);
currentLengthOfPath++;
}
else
{
if (currentLengthOfPath > lengthOfPathToEndField)
{
endField = field;
lengthOfPathToEndField = currentLengthOfPath;
}
field = field.getPreviousField ();
currentLengthOfPath--;
}
findNeighbours (field); // recursive call
}
}
/* Method buildMaze for building the maze */
public void buildMaze () throws MazeException
{
setBorders ();
startField = findFirstField ();
endField = startField;
findNeighbours (startField);
mazeArray.checkMaze ();
this.init ();
System.out.println ("Maze is built, path length from start to end is " + lengthOfPathToEndField + ".");
}
/* Overwritten method "paint" of super class java.awt.Window -> java.awt.Frame -> javax.swing.JFrame */
public void paint (java.awt.Graphics graphics)
{
drawMaze (graphics, mazeArray);
drawMarkInField (graphics, this.startField, java.awt.Color.GREEN);
drawMarkInField (graphics, this.endField, java.awt.Color.ORANGE);
}
}