Reputation: 97
I've written some code for generating mazes, it uses the Recursive Backtracking algorithm. I've also written a class for visualizing the maze, but as of now, the DrawMaze class only outputs the finished maze. But I would like to see the whole process of building the maze.
The maze gets generated with the RecursiveBacktracker.java class. And is sent into DrawMaze.java as a 2d-array of Nodes.
How can I make JFrame update on every iteration (for both i & j) in the DrawMaze paintComponent method?
RecursiveBacktracking.java
import java.util.*;
public class RecursiveBacktracking {
Stack<Node> stack = new Stack<>();
int w;
int h;
int numVisited;
Node[][] maze;
public RecursiveBacktracking(int w, int h){
// Constructor
}
public void genMaze() {
// Generates the maze using recursive backtracking
}
public void graphMaze() {
DrawMaze dm = new DrawMaze(w, h, maze);
dm.showMaze();
}
public static void main(String[] args) {
RecursiveBacktracking rb = new RecursiveBacktracking(20, 20);
rb.genMaze();
rb.graphMaze();
}
}
DrawMaze.java:
import java.awt.*;
import javax.swing.*;
public class DrawMaze extends JPanel {
Node[][] maze;
int width;
int height;
public DrawMaze(int width, int height, Node[][] maze){
this.maze = maze;
this.width = width;
this.height = height;
}
public void paintComponent(Graphics g){
// EVERY TIME i or j CHANGE VALUE
// JFrame should update
for(int i = 0; i < width; i++){
for(int j = 0; j < height; j++){
// Draws all nodes & their walls
}
}
}
public void showMaze() {
JFrame f = new JFrame("Maze Visualizer");
f.add(this);
f.setSize(width, height);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
Upvotes: 0
Views: 378
Reputation: 347204
Trying to visualise a recursive workflow is not a simple matter. There are so many questions which need to be answered and most recursive workflows aren't meant to be monitored in this way. You could implement a visitor pattern to help provide some feedback, but this only helps slightly in solving this problem.
Swing uses a passive rendering workflow. That is, it will update only when it thinks it needs to, unlike say, a video game, which is constantly updating the UI.
So, you need some way to get the current state, stop the recursion from executing for a short period of time (otherwise the user won't see what happens) and repaint the UI, all without violating the single threaded nature Swing ... easy 🥴
The long answer is, you're going to need to design for this. You might be able to use some kind of "wrapper" class which can control the recursion some how while providing feedback, but you'd still need some way to "control" the recursion flow and get feedback about it's state in some kind of meaningful way (without just throwing a bunch of Thread.sleep
s in the code and hoping it works)
nb: I stole the core logic for my maze solver from Program for Rat in a Maze | Backtracking-2. The point here isn't to try and solve the maze solving workflow, but to present some ideas about how you might visualise the workflow
SwingWorker
One approach might be to use a little bit to brute force. This employees a observer pattern which is notified when the position changes (and when it completes, but it's not required) and triggers a update to the UI. When ever the position in the maze changes, the SwingWorker
is stopped for 1 second to allow the UI to update and the user to monitor the changes
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingWorker;
public class TestRecursiveMaze {
public static void main(String[] args) {
new TestRecursiveMaze();
}
public TestRecursiveMaze() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
MazeSolverPane testPane = new MazeSolverPane();
JPanel panel = new JPanel(new GridBagLayout());
JButton start = new JButton("Start");
panel.add(start);
start.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
start.setEnabled(false);
testPane.startSolving(new MazeSolverPane.SolverListener() {
@Override
public void solverDidComplete() {
start.setEnabled(true);
}
});
}
});
JFrame frame = new JFrame();
frame.add(testPane);
frame.add(panel, BorderLayout.SOUTH);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class MazeSolverPane extends JPanel {
public interface SolverListener {
public void solverDidComplete();
}
private RecursiveMazeSolver solver;
private Maze maze;
private Rectangle cell = new Rectangle(25, 25);
private int[] lastKnownPoint;
public MazeSolverPane() {
int pattern[][] = {
{1, 0, 0, 1},
{1, 1, 0, 1},
{0, 1, 1, 1},
{1, 1, 0, 1}
};
maze = new Maze(pattern, 0, 0, 3, 0);
}
public void startSolving(SolverListener listener) {
if (solver != null) {
return;
}
solver = new RecursiveMazeSolver(maze);
repaint();
SwingWorker<Void, int[]> worker = new SwingWorker<Void, int[]>() {
@Override
protected Void doInBackground() throws Exception {
solver.solveMaze(new RecursiveMazeSolver.SolverListener() {
@Override
public void solverDidMoveTo(RecursiveMazeSolver solver, int x, int y) {
publish(new int[] { x, y });
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
}
}
@Override
public void solverDidFinish(RecursiveMazeSolver solver) {
}
});
return null;
}
@Override
protected void process(List<int[]> chunks) {
lastKnownPoint = chunks.get(chunks.size() - 1);
repaint();
}
};
worker.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (worker.getState() == SwingWorker.StateValue.DONE) {
listener.solverDidComplete();
}
}
});
worker.execute();
}
public Maze getMaze() {
return maze;
}
public RecursiveMazeSolver getSolver() {
return solver;
}
@Override
public Dimension getPreferredSize() {
Maze maze = getMaze();
return new Dimension((cell.width * maze.getWidth()) + 1, (cell.height * maze.getHeight()) + 1);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Maze maze = getMaze();
for (int y = 0; y < maze.getHeight(); y++) {
for (int x = 0; x < maze.getWidth(); x++) {
Graphics2D g2d = (Graphics2D) g.create();
int xPos = x * (cell.width);
int yPos = y * (cell.height);
g2d.translate(xPos, yPos);
RecursiveMazeSolver solver = getSolver();
if (lastKnownPoint != null && lastKnownPoint[0] == x && lastKnownPoint[1] == y) {
g2d.setColor(Color.MAGENTA);
} else if (solver != null && solver.hasBreadCrumbAt(x, y)) {
g2d.setColor(Color.YELLOW);
} else if (maze.isWallAt(x, y)) {
g2d.setColor(Color.DARK_GRAY);
} else if (maze.isStart(x, y)) {
g2d.setColor(Color.GREEN);
} else if (maze.isEnd(x, y)) {
g2d.setColor(Color.RED);
} else {
g2d.setColor(getBackground());
}
g2d.fill(cell);
g2d.setColor(getForeground());
g2d.draw(cell);
g2d.dispose();
}
}
}
}
public class Maze {
protected static final int WALL = 0;
protected static final int PATH = 1;
private int[][] maze;
private int startX, startY;
private int endX, endY;
public Maze(int[][] maze, int startX, int startY, int endX, int endY) {
this.maze = maze;
this.startX = startX;
this.startY = startY;
this.endX = endX;
this.endY = endY;
}
public boolean isStart(int x, int y) {
return x == getStartX() && y == getStartY();
}
public boolean isEnd(int x, int y) {
return x == getEndX() && y == getEndY();
}
public int[][] getMaze() {
return maze;
}
public int getStartX() {
return startX;
}
public int getStartY() {
return startY;
}
public int getEndX() {
return endX;
}
public int getEndY() {
return endY;
}
public boolean isWallAt(int x, int y) {
return getMaze()[y][x] == WALL;
}
public boolean isPathAt(int x, int y) {
return getMaze()[y][x] == PATH;
}
public int getWidth() {
return getMaze()[0].length;
}
public int getHeight() {
return getMaze().length;
}
}
public class RecursiveMazeSolver {
public interface SolverListener {
public void solverDidMoveTo(RecursiveMazeSolver solver, int x, int y);
public void solverDidFinish(RecursiveMazeSolver solver);
}
private Maze maze;
private int sol[][];
public RecursiveMazeSolver(Maze maze) {
this.maze = maze;
}
public Maze getMaze() {
return maze;
}
protected void setBreadCrumbAt(int x, int y) {
sol[y][x] = 1;
}
protected void removeBreadCrumbAt(int x, int y) {
sol[y][x] = 0;
}
public boolean hasBreadCrumbAt(int x, int y) {
return sol == null ? false : sol[y][x] == 1;
}
public void solveMaze(SolverListener listener) {
Maze maze = getMaze();
sol = new int[maze.getWidth()][maze.getHeight()];
solveMaze(maze.getStartX(), maze.getStartY(), listener);
}
protected boolean solveMaze(int x, int y, SolverListener listener) {
Maze maze = getMaze();
if ((x < 0 || x >= maze.getWidth())) {
return false;
}
if ((y < 0 || y >= maze.getHeight())) {
return false;
}
if (x == maze.getEndX() && y == maze.getEndY()) {
setBreadCrumbAt(x, y);
listener.solverDidMoveTo(this, x, y);
listener.solverDidFinish(this);
return true;
}
if (maze.isPathAt(x, y) && !hasBreadCrumbAt(x, y)) {
setBreadCrumbAt(x, y);
listener.solverDidMoveTo(this, x, y);
if (solveMaze(x + 1, y, listener)) {
return true;
}
if (solveMaze(x, y + 1, listener)) {
return true;
}
if (solveMaze(x - 1, y, listener)) {
return true;
}
if (solveMaze(x, y - 1, listener)) {
return true;
}
removeBreadCrumbAt(x, y);
}
return false;
}
}
}
See Worker Threads and SwingWorker for more details
The following uses a "controlled" workflow. This is one where each stop is controlled externally, so that each call to next
will make one more iteration towards solving the maze. This can be done when ever the caller wants to and allows a far greater level of control, as the caller decides when it wants to move to the next iteration.
While the example makes use of Swing Timer
, you could use a "next" button instead.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.StringJoiner;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class TestSteppedMaze {
public static void main(String[] args) {
new TestSteppedMaze();
}
public TestSteppedMaze() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
MazeSolverPane testPane = new MazeSolverPane();
JPanel panel = new JPanel(new GridBagLayout());
JButton start = new JButton("Start");
panel.add(start);
start.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
start.setEnabled(false);
testPane.startSolving(new MazeSolverPane.SolverListener() {
@Override
public void solverDidComplete() {
start.setEnabled(true);
}
});
}
});
JFrame frame = new JFrame();
frame.add(testPane);
frame.add(panel, BorderLayout.SOUTH);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class MazeSolverPane extends JPanel {
public interface SolverListener {
public void solverDidComplete();
}
private SteppedMazeSolver solver;
private Maze maze;
private Rectangle cell = new Rectangle(25, 25);
public MazeSolverPane() {
int pattern[][] = {
{1, 0, 0, 1},
{1, 1, 0, 1},
{0, 1, 1, 1},
{1, 1, 0, 1}
};
maze = new Maze(pattern, 0, 0, 3, 0);
}
public void startSolving(SolverListener listener) {
if (solver != null) {
return;
}
solver = new SteppedMazeSolver(maze);
repaint();
Timer timer = new Timer(1000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (solver.next()) {
((Timer) e.getSource()).stop();
listener.solverDidComplete();
}
repaint();
}
});
timer.start();
}
public Maze getMaze() {
return maze;
}
public SteppedMazeSolver getSolver() {
return solver;
}
@Override
public Dimension getPreferredSize() {
Maze maze = getMaze();
return new Dimension((cell.width * maze.getWidth()) + 1, (cell.height * maze.getHeight()) + 1);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Maze maze = getMaze();
for (int y = 0; y < maze.getHeight(); y++) {
for (int x = 0; x < maze.getWidth(); x++) {
Graphics2D g2d = (Graphics2D) g.create();
int xPos = x * (cell.width);
int yPos = y * (cell.height);
g2d.translate(xPos, yPos);
if (solver != null && solver.getCurrentPoint().equals(new Point(x, y))) {
g2d.setColor(Color.MAGENTA);
} else if (solver != null && solver.isVisited(x, y)) {
g2d.setColor(Color.YELLOW);
} else if (maze.isWallAt(x, y)) {
g2d.setColor(Color.DARK_GRAY);
} else if (maze.isStart(x, y)) {
g2d.setColor(Color.GREEN);
} else if (maze.isEnd(x, y)) {
g2d.setColor(Color.RED);
} else {
g2d.setColor(getBackground());
}
g2d.fill(cell);
g2d.setColor(getForeground());
g2d.draw(cell);
g2d.dispose();
}
}
}
}
public class Maze {
protected static final int WALL = 0;
protected static final int PATH = 1;
private int[][] maze;
private int startX, startY;
private int endX, endY;
public Maze(int[][] maze, int startX, int startY, int endX, int endY) {
this.maze = maze;
this.startX = startX;
this.startY = startY;
this.endX = endX;
this.endY = endY;
}
public boolean isStart(int x, int y) {
return x == getStartX() && y == getStartY();
}
public boolean isEnd(int x, int y) {
return x == getEndX() && y == getEndY();
}
public int[][] getMaze() {
return maze;
}
public int getStartX() {
return startX;
}
public int getStartY() {
return startY;
}
public int getEndX() {
return endX;
}
public int getEndY() {
return endY;
}
public boolean isWallAt(int x, int y) {
return getMaze()[y][x] == WALL;
}
public boolean isPathAt(int x, int y) {
return getMaze()[y][x] == PATH;
}
public int getWidth() {
return getMaze()[0].length;
}
public int getHeight() {
return getMaze().length;
}
@Override
public String toString() {
StringJoiner outter = new StringJoiner("\n");
for (int y = 0; y < getHeight(); y++) {
StringBuilder sb = new StringBuilder(getWidth());
for (int x = 0; x < getWidth(); x++) {
sb.append(Integer.toString(maze[y][x]));
}
outter.add(sb);
}
return outter.toString();
}
}
public class Point {
private int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public Point delta(int x, int y) {
return new Point(getX() + x, getY() + y);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point)) {
return false;
}
Point other = (Point) obj;
if (other == this) {
return true;
} else if (other.getX() == getX() && other.getY() == getY()) {
return true;
}
return false;
}
@Override
public String toString() {
return getX() + "x" + getY();
}
}
public class SteppedMazeSolver {
private Maze maze;
private Deque<Point> visited;
private List<Point> blocked;
private Point currentPoint;
public SteppedMazeSolver(Maze maze) {
this.maze = maze;
visited = new ArrayDeque<>();
blocked = new ArrayList<>(16);
currentPoint = new Point(maze.getStartX(), maze.getStartY());
visited.add(currentPoint);
}
public Maze getMaze() {
return maze;
}
public Point getCurrentPoint() {
return currentPoint;
}
public boolean isVisited(int x, int y) {
Iterator<Point> iterator = visited.iterator();
while (iterator.hasNext()) {
Point point = iterator.next();
if (point.getX() == x && point.getY() == y) {
return true;
}
}
return false;
}
protected boolean isAtEnd() {
Maze maze = getMaze();
return currentPoint.getX() == maze.getEndX() && currentPoint.getY() == maze.getEndY();
}
public boolean next() {
if (isAtEnd()) {
System.out.println("I've escaped");
return true;
}
if (canMoveRight()) {
System.out.println("Right");
currentPoint = currentPoint.delta(1, 0);
visited.add(currentPoint);
} else if (canMoveLeft()) {
System.out.println("Left");
currentPoint = currentPoint.delta(-1, 0);
visited.add(currentPoint);
} else if (canMoveDown()) {
System.out.println("Down");
currentPoint = currentPoint.delta(0, 1);
visited.add(currentPoint);
} else if (canMoveUp()) {
System.out.println("Up");
currentPoint = currentPoint.delta(0, -1);
visited.add(currentPoint);
} else {
System.out.println("Blocked at " + currentPoint);
blocked.add(currentPoint);
visited.removeLast();
currentPoint = visited.getLast();
}
return isAtEnd();
}
protected boolean canMoveRight() {
return canMoveTo(1, 0);
}
protected boolean canMoveLeft() {
return canMoveTo(-1, 0);
}
protected boolean canMoveUp() {
return canMoveTo(0, -1);
}
protected boolean canMoveDown() {
return canMoveTo(0, 1);
}
protected boolean canMoveTo(int xDelta, int yDelta) {
Point nextPoint = currentPoint.delta(xDelta, yDelta);
if (nextPoint.getX() < 0 || nextPoint.getY() < 0) {
return false;
}
Maze maze = getMaze();
if (nextPoint.getX() >= maze.getWidth() || nextPoint.getY() >= maze.getHeight()) {
return false;
}
if (blocked.contains(nextPoint)) {
return false;
}
if (visited.contains(nextPoint)) {
return false;
}
return maze.isPathAt(nextPoint.getX(), nextPoint.getY());
}
}
}
Before any one jumps down my throat, no, this is not a "recursive" solution in the normal sense. Rather than the next
method continuously calling itself till it's done, an external controller is doing it.
See How to Use Swing Timers for more details.
Well, to be honest, neither really. Both are making compromises somewhere in order to facilitate the ability to present their state changes. You could also have a "monitor lock" based solution, but you're still compromising the underlying solution in order to present the state.
A "better"(ish) solution would be one which was deliberately designed to provide some kind of feedback to the user (ie using a visitor pattern), which would be able to provide state information and the opportunity for the user to "pause" the thread, while not otherwise compromising the core algorithm. While the first solution "kind of" does this, you can clearly see it's a still a lot of work to maintain and get it working right (in fact, I think I missed a reversal update somewhere :/)
Upvotes: 1