import java.awt.Color; import java.awt.Graphics; import java.awt.Image; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.applet.Applet; /** *

Program allowing player to navigate a four dimensional labyrinth. This * program draws a board sized to what is specified in its parameters or in the * default values if parameters are not given; it may therefore be larger or * smaller than the applet window that it is put in.

* *

This program is a Java re-implementation of a maze originally written by * Jonathan Hayward in eighth grade, in Apple ][ BASIC, and re-implemented in C * during undergraduate studies. It retains a classic style of graphics and * the original algorithms and basic data structures, but adds a recursive * solvability verifier, configurability in invocation that was impossible * under previous implementations, and a point-and-click interface. The * applet may be seen at * http://JonathansCorner.com/etc/maze/; * it was slashdotted within a week of going online.

* *

This is presented as a code sample for:

* *

Jonathan Hayward
* jonathan.hayward@pobox.com

* *

Dimensions are as follows:

*
 * Within squares:
 *
 *  ^
 * Y|
 *  +->
 *   X
 *
 * Between squares:
 *
 *  ^
 * Z|
 *  +->
 *   W
 * 
* *

This is disanalogous, but gives the most obvious place to the fourth * (spatial) dimension after the first three dimensions have been assigned * their usual letters. Therefore, I am keeping it that way.

* *

Thanks to my father, John Hayward, for help with an AWT bug, James * Holt for a bugfix and additional code to address bug which manifested in * larger mazes, and Darren Edmonds for pointers on how to update the * MouseEvent handling.

* * @author Jonathan Hayward */ public class Maze4D extends Applet implements MouseListener, Runnable { private static final boolean PASSABLE = true, IMPASSABLE = false; private boolean noDrawMaze = false; private boolean[][][][] maze = null; public static final long serialVersionUID = 0; private static final int X = 0, Y = 1, Z = 2, W = 3, DEFAULT_SIZE = 4, DIMENSION = 4, FAILED_MOVE_OFF_MAZE = 5, FAILED_MOVE_IMPASSABLE = 6; // Next code 7 private int borderWidth = 40, borderHeight = 40, slotWidth = 20, slotHeight = 20; private int[] size = new int[4], playerPosition = new int[4]; private double probabilityPassable = 0.5; private Color backgroundColor = Color.black, impassableColor = Color.red, passableColor = Color.white, playerColor = Color.blue, endLocationColor = Color.green; private Graphics givenGraphics = null, offScreenGraphics = null; private Image offScreenImage = null; /** *

Attempt to move the player by a given vector relative to his present * position. Will succeed if the move is to a legal board space and the * space is passable.

*/ private void attemptToMove(int xDiff, int yDiff, int zDiff, int wDiff) { if ((0 <= playerPosition[X] + xDiff) && (playerPosition[X] + xDiff < size[X]) && (0 <= playerPosition[Y] + yDiff) && (playerPosition[Y] + yDiff < size[Y]) && (0 <= playerPosition[Z] + zDiff) && (playerPosition[Z] + zDiff < size[Z]) && (0 <= playerPosition[W] + wDiff) && (playerPosition[W] + wDiff < size[W])) { if (maze[playerPosition[X] + xDiff] [playerPosition[Y] + yDiff] [playerPosition[Z] + zDiff] [playerPosition[W] + wDiff] == PASSABLE) { playerPosition[X] += xDiff; playerPosition[Y] += yDiff; playerPosition[Z] += zDiff; playerPosition[W] += wDiff; drawBoard(); } else { failedMove(FAILED_MOVE_IMPASSABLE); } } else { failedMove(FAILED_MOVE_OFF_MAZE); } } /** *

Calculate the height of the board, in pixels.

*/ private int boardHeight() { return 2 * borderHeight + (size[Y] + 1) * size[Z] * slotHeight; } /** *

Calculate the width of the board, in pixels.

*/ private int boardWidth() { return 2 * borderWidth + ((size[X] + 1) * size[W]) * slotWidth; } /** *

Calculate the screen x coordinate of the lower lefthand corner of a * square * representing a slot in the maze. CornerX and cornerY both take * more information than they need, in an information hiding fashion to * indicate "This is how I get the graphical x coordinate for a * hyperspace point", and not "Which two arguments go into the * formula?"

*/ private int cornerX(int x, int y, int z, int w) { return borderWidth + (x + w * (size[X] + 1)) * slotWidth; } /** *

Calculate the y coordinate of the lower lefthand corner of a square * representing a slot in the maze. CornerX and cornerY both take more * information than they need, in an information hiding fashion to indicate * "This is how I get the graphical y coordinate for a hyperspace point", * and not "Which two arguments go into the formula?"

*/ private int cornerY(int x, int y, int z, int w) { return boardHeight() - (borderHeight + (y + z * (size[Y] + 1) + 2) * slotHeight); } /** *

Draw the board on the screen.

*/ public void drawBoard() { if (!noDrawMaze) { offScreenGraphics.setColor(backgroundColor); offScreenGraphics.fillRect(0, 0, boardWidth() + 1, boardHeight() + 1); for(int i = 0; i < size[X]; ++i) for(int j = 0; j < size[Y]; ++j) for(int k = 0; k < size[Z]; ++k) for(int l = 0; l < size[W]; ++l) { if (maze[i][j][k][l] == PASSABLE) offScreenGraphics.setColor(passableColor); else offScreenGraphics.setColor(impassableColor); if ((i == size[X] - 1) && (j == size[Y] - 1) && (k == size[Z] - 1) && (l == size[W] - 1)) offScreenGraphics.setColor(endLocationColor); offScreenGraphics.fillRect(cornerX(i, j, k, l), cornerY(i, j, k, l), slotWidth, slotHeight); } offScreenGraphics.setColor(playerColor); int playerX = cornerX(playerPosition[X], playerPosition[Y], playerPosition[Z], playerPosition[W]); int playerY = cornerY(playerPosition[X], playerPosition[Y], playerPosition[Z], playerPosition[W]); offScreenGraphics.fillRect(playerX, playerY, slotWidth, slotHeight); givenGraphics = this.getGraphics(); if (givenGraphics != null) givenGraphics.drawImage(offScreenImage, 0, 0, this); validate(); } } /** *

Method invoked when the player attempts an illegal move. This hook * is presently unimplemented.

*/ private void failedMove(int failureCode) { // At present, do nothing. If desired, an error message can be // displayed to the user. } /** *

Generate a solveable maze. The original eighth-grade program did * not check to see if the generated maze was solveable.

*/ private void generateMaze() { maze = new boolean[size[X]][size[Y]][size[Z]][size[W]]; for(int i = 0; i < size[X]; ++i) for(int j = 0; j < size[Y]; ++j) for(int k = 0; k < size[Z]; ++k) for(int l = 0; l < size[W]; ++l) if (Math.random() < probabilityPassable) maze[i][j][k][l] = PASSABLE; else maze[i][j][k][l] = IMPASSABLE; maze[0][0][0][0] = PASSABLE; maze[size[X] - 1][size[Y] - 1][size[Z] - 1][size[W] - 1] = PASSABLE; if (!mazeIsSolveable()) generateMaze(); } /** *

Initializations performed when the applet is created.

*/ public void init() { size[X] = readInt("X", DEFAULT_SIZE); size[Y] = readInt("Y", DEFAULT_SIZE); size[Z] = readInt("Z", DEFAULT_SIZE); size[W] = readInt("W", DEFAULT_SIZE); playerPosition[X] = playerPosition[Y] = playerPosition[Z] = playerPosition[W] = 0; probabilityPassable = readDouble("Probability", probabilityPassable); generateMaze(); borderHeight = readInt("BorderHeight", borderHeight); borderWidth = readInt("BorderWidth", borderWidth); slotHeight = readInt("SlotHeight", slotHeight); slotWidth = readInt("SlotWidth", slotWidth); offScreenImage = createImage(boardWidth(), boardHeight()); offScreenGraphics = offScreenImage.getGraphics(); // enableEvents(AWTEvent.MOUSE_EVENT_MASK); addMouseListener(this); } /** *

Recursive helper function to mark all slots reachable from a given * slot. Used in seeing if the maze is navigable.

*/ private void markReached(boolean[][][][] reached, int x, int y, int z, int w) { reached[x][y][z][w] = true; if (x - 1 >= 0) if (maze[x - 1][y][z][w] == PASSABLE && !reached[x - 1][y][z][w]) markReached(reached, x - 1, y, z, w); if (x + 1 < size[X]) if (maze[x + 1][y][z][w] == PASSABLE && !reached[x + 1][y][z][w]) markReached(reached, x + 1, y, z, w); if (y - 1 >= 0) if (maze[x][y - 1][z][w] == PASSABLE && !reached[x][y - 1][z][w]) markReached(reached, x, y - 1, z, w); if (y + 1 < size[Y]) if (maze[x][y + 1][z][w] == PASSABLE && !reached[x][y + 1][z][w]) markReached(reached, x, y + 1, z, w); if (z - 1 >= 0) if (maze[x][y][z - 1][w] == PASSABLE && !reached[x][y][z - 1][w]) markReached(reached, x, y, z - 1, w); if (z + 1 < size[Z]) if (maze[x][y][z + 1][w] == PASSABLE && !reached[x][y][z + 1][w]) markReached(reached, x, y, z + 1, w); if (w - 1 >= 0) if (maze[x][y][z][w - 1] == PASSABLE && !reached[x][y][z][w - 1]) markReached(reached, x, y, z, w - 1); if (w + 1 < size[W]) if (maze[x][y][z][w + 1] == PASSABLE && !reached[x][y][z][w + 1]) markReached(reached, x, y, z, w + 1); } /** *

Method to determine if a given maze is solveable.

*/ private boolean mazeIsSolveable() { boolean[][][][] reached = new boolean[size[X]][size[Y]][size[Z]][size[W]]; for(int i = 0; i < size[X]; ++i) for(int j = 0; j < size[Y]; ++j) for(int k = 0; k < size[Z]; ++k) for(int l = 0; l < size[W]; ++l) reached[i][j][k][l] = false; markReached(reached, 0, 0, 0, 0); return reached[size[X] - 1][size[Y] - 1][size[Z] - 1][size[W] - 1]; } /** *

Process a mouse click to see if it is corresponds to a legal move, * and, if so, make that move. A legal move is one that takes to a space * adjacent to his present space in hyperspace (not necessarily on the * board).

*/ public void mouseClicked(MouseEvent theEvent) { if (theEvent.getY() < borderHeight || theEvent.getY() > boardHeight() - borderHeight || theEvent.getX() < borderWidth || theEvent.getX() > boardWidth() - borderWidth) { return; } int x = ( ( theEvent.getX() - borderWidth ) / slotWidth ) % (size[X] + 1), w = ( ( theEvent.getX() - borderWidth ) / slotWidth ) / (size[X] + 1), y = ( ( ( boardHeight() - borderHeight - theEvent.getY() ) / slotHeight ) % (size[Y] + 1) ) - 1, z = ( ( boardHeight() - borderHeight - theEvent.getY() ) / slotHeight ) / (size[Y] + 1); if ((0 <= x && x < size[X]) && (0 <= y && y < size[Y]) && (0 <= z && z < size[Z]) && (0 <= w && w < size[W])) { int xDiff = x - playerPosition[X], yDiff = y - playerPosition[Y], zDiff = z - playerPosition[Z], wDiff = w - playerPosition[W]; // Only move to adjacent spaces. if (Math.abs(xDiff) + Math.abs(yDiff) + Math.abs(zDiff) + Math.abs(wDiff) < 2) { attemptToMove(xDiff, yDiff, zDiff, wDiff); } } testWin(); } /** *

Respond appropriately to the mouse cursor entering the component. At * present, this does nothing.

*/ public void mouseEntered(MouseEvent theEvent) { } /** *

Respond appropriately to the mouse cursor exiting the component. At * present, this does nothing.

*/ public void mouseExited(MouseEvent theEvent) { } /** *

Respond appropriately to the mouse button being pressed. At * present, this does nothing.

*/ public void mousePressed(MouseEvent theEvent) { } /** *

Respond appropriately to the mouse button being released. At * present, this does nothing.

*/ public void mouseReleased(MouseEvent theEvent) { } /** *

This method is called when the display is to be painted.

*/ public void paint(Graphics g) { drawBoard(); } /** *

Read a string for a given double value, returning specified default * if there is a parse error.

*/ private double readDouble(String toReadFrom, double defaultValue) { try { return new Double(getParameter(toReadFrom)).doubleValue(); } catch(Exception e) { return defaultValue; } } /** *

Read a string for a given integer value, returning specified default * if there is a parse error.

*/ private int readInt(String toReadFrom, int defaultValue) { try { return new Integer(getParameter(toReadFrom)).intValue(); } catch(Exception e) { return defaultValue; } } /** *

Display a success message for two seconds before generating a new * maze. This is done in a separate thread because if it is done in the * mouse event handler, the screen is not updated until the end of the two * seconds, where it flashes, subliminal message style.

*/ public void run() { noDrawMaze = true; offScreenGraphics.setColor(Color.white); offScreenGraphics.fillRect(borderWidth, borderHeight, boardWidth() - 2 * borderWidth - slotWidth, boardHeight() - 2 * borderHeight - slotHeight); offScreenGraphics.setColor(Color.black); offScreenGraphics.drawString( "Congratulations, you've solved the maze!", borderWidth * 2, borderHeight * 2); givenGraphics = this.getGraphics(); givenGraphics.drawImage(offScreenImage, 0, 0, this); validate(); try { Thread.sleep(2000); } catch(InterruptedException e) { } noDrawMaze = false; drawBoard(); } /** *

Test to see if the player has won, and, if so, congratulate the * player and create a new maze.

*/ private void testWin() { if (playerPosition[X] == size[X] - 1 && playerPosition[Y] == size[Y] - 1 && playerPosition[Z] == size[Z] - 1 && playerPosition[W] == size[W] - 1) { playerPosition[X] = playerPosition[Y] = playerPosition[Z] = playerPosition[W] = 0; generateMaze(); Thread displayWinMessage = new Thread(this); displayWinMessage.start(); } } /** *

This applet method is called to update the contents of the * screen.

*/ public void update(Graphics g) { drawBoard(); } }