Initial - and extremely ugly - version of Wilson's maze algorithm.
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
cf405fbc98
commit
cdfa1f4476
8 changed files with 360 additions and 10 deletions
|
@ -0,0 +1,19 @@
|
|||
package ch.fritteli.maze.generator.algorithm;
|
||||
|
||||
import ch.fritteli.maze.generator.model.Maze;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public abstract class AbstractMazeGeneratorAlgorithm implements MazeGeneratorAlgorithm {
|
||||
@NotNull
|
||||
protected final Maze maze;
|
||||
@NotNull
|
||||
protected final Random random;
|
||||
|
||||
|
||||
public AbstractMazeGeneratorAlgorithm(@NotNull final Maze maze) {
|
||||
this.maze = maze;
|
||||
this.random = new Random(maze.getRandomSeed());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package ch.fritteli.maze.generator.algorithm;
|
||||
|
||||
public interface MazeGeneratorAlgorithm {
|
||||
void run();
|
||||
}
|
|
@ -10,20 +10,13 @@ import org.jetbrains.annotations.Nullable;
|
|||
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Random;
|
||||
|
||||
public class RandomDepthFirst {
|
||||
|
||||
@NotNull
|
||||
private final Maze maze;
|
||||
@NotNull
|
||||
private final Random random;
|
||||
public class RandomDepthFirst extends AbstractMazeGeneratorAlgorithm {
|
||||
@NotNull
|
||||
private final Deque<Position> positions = new LinkedList<>();
|
||||
|
||||
public RandomDepthFirst(@NotNull final Maze maze) {
|
||||
this.maze = maze;
|
||||
this.random = new Random(maze.getRandomSeed());
|
||||
super(maze);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
|
|
297
src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
Normal file
297
src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java
Normal file
|
@ -0,0 +1,297 @@
|
|||
package ch.fritteli.maze.generator.algorithm;
|
||||
|
||||
import ch.fritteli.maze.generator.model.Direction;
|
||||
import ch.fritteli.maze.generator.model.Maze;
|
||||
import ch.fritteli.maze.generator.model.Position;
|
||||
import ch.fritteli.maze.generator.model.Tile;
|
||||
import io.vavr.Tuple2;
|
||||
import io.vavr.collection.Iterator;
|
||||
import io.vavr.collection.Seq;
|
||||
import io.vavr.collection.Stream;
|
||||
import io.vavr.collection.Vector;
|
||||
import io.vavr.control.Option;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* An implementation of <a href="https://en.wikipedia.org/wiki/Maze_generation_algorithm#Wilson's_algorithm">Wilson's Algorithm</a>.
|
||||
*/
|
||||
public class Wilson extends AbstractMazeGeneratorAlgorithm {
|
||||
public Wilson(@NotNull final Maze maze) {
|
||||
super(maze);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final MyMaze myMaze = new MyMaze(this.maze.getWidth(), this.maze.getHeight());
|
||||
// 1. Initialization: pick random location, add to maze
|
||||
final Position position = myMaze.getRandomAvailablePosition(this.random).get();
|
||||
myMaze.setPartOfMaze(position);
|
||||
|
||||
// 2. while locations-not-in-maze exist, repeat:
|
||||
|
||||
final List<MyMaze.Path> pths = new ArrayList<>();
|
||||
Position startPosition;
|
||||
while ((startPosition = myMaze.getRandomAvailablePosition(this.random).getOrNull()) != null) {
|
||||
final MyMaze.Path path = myMaze.createPath(startPosition);
|
||||
while (true) {
|
||||
final Position nextPosition = path.nextRandomPosition(this.random);
|
||||
if (path.contains(nextPosition)) {
|
||||
path.removeLoopUpTo(nextPosition);
|
||||
} else {
|
||||
path.append(nextPosition);
|
||||
if (myMaze.isPartOfMaze(nextPosition)) {
|
||||
myMaze.setPartOfMaze(path);
|
||||
pths.add(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
applyToMaze(pths, maze);
|
||||
solve(maze);
|
||||
// 1. pick random location not in maze
|
||||
// 2. perform random walk until you reach the maze (*)
|
||||
// 3. add path to maze
|
||||
// (*): random walk:
|
||||
// 1. advance in random direction
|
||||
// 2. if loop with current path is formed, remove loop, continue from last location before loop
|
||||
}
|
||||
|
||||
private void applyToMaze(@NotNull final List<MyMaze.Path> pths, @NotNull final Maze maze) {
|
||||
pths.forEach(path -> {
|
||||
final Iterator<Direction> moves = Stream.ofAll(path.path)
|
||||
.sliding(2)
|
||||
.flatMap(poss -> poss.head().getDirectionTo(poss.get(1)));
|
||||
Position position = path.path.getFirst();
|
||||
while (moves.hasNext()) {
|
||||
Tile tile = maze.getTileAt(position).get();
|
||||
final Direction move = moves.next();
|
||||
tile.digTo(move);
|
||||
position = position.move(move);
|
||||
maze.getTileAt(position).forEach(t -> t.digTo(move.invert()));
|
||||
}
|
||||
});
|
||||
Direction direction = determineDirectionForDigging(maze.getStart());
|
||||
Tile t = maze.getStartTile();
|
||||
this.digTo(t, direction);
|
||||
direction = determineDirectionForDigging(maze.getEnd());
|
||||
t = maze.getEndTile();
|
||||
this.digTo(t, direction);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Direction determineDirectionForDigging(@NotNull final Position position) {
|
||||
if (position.y() == 0) {
|
||||
return Direction.TOP;
|
||||
}
|
||||
if (position.x() == 0) {
|
||||
return Direction.LEFT;
|
||||
}
|
||||
if (position.y() == this.maze.getHeight() - 1) {
|
||||
return Direction.BOTTOM;
|
||||
}
|
||||
if (position.x() == this.maze.getWidth() - 1) {
|
||||
return Direction.RIGHT;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void digTo(@NotNull final Tile tile, @NotNull final Direction direction) {
|
||||
tile.enableDiggingToOrFrom(direction);
|
||||
tile.digTo(direction);
|
||||
}
|
||||
|
||||
private void solve(@NotNull final Maze maze) {
|
||||
EnumSet<Direction>[][] remainingDirs = new EnumSet[maze.getWidth()][maze.getHeight()];
|
||||
for (int x = 0; x < remainingDirs.length; x++) {
|
||||
for (int y = 0; y < remainingDirs[x].length; y++) {
|
||||
remainingDirs[x][y] = EnumSet.allOf(Direction.class);
|
||||
}
|
||||
}
|
||||
Position p = maze.getStart();
|
||||
final Direction direction = this.determineDirectionForDigging(p);
|
||||
remainingDirs[p.x()][p.y()].remove(direction);
|
||||
LinkedList<Position> solution = new LinkedList<>();
|
||||
solution.add(p);
|
||||
while (!p.equals(maze.getEnd())) {
|
||||
final Tile tile = maze.getTileAt(p).get();
|
||||
EnumSet<Direction> dirs = remainingDirs[p.x()][p.y()];
|
||||
dirs.removeIf(tile::hasWallAt);
|
||||
if (dirs.isEmpty()) {
|
||||
solution.pop();
|
||||
p = solution.peek();
|
||||
} else {
|
||||
final Direction nextDir = dirs.iterator().next();
|
||||
final Position nextPos = p.move(nextDir);
|
||||
solution.push(nextPos);
|
||||
remainingDirs[p.x()][p.y()].remove(nextDir);
|
||||
remainingDirs[nextPos.x()][nextPos.y()].remove(nextDir.invert());
|
||||
p = nextPos;
|
||||
}
|
||||
}
|
||||
solution.forEach(s -> maze.getTileAt(s).forEach(Tile::setSolution));
|
||||
}
|
||||
|
||||
private static class MyMaze {
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final boolean[][] partOfMaze;
|
||||
private final boolean[] completeColumns;
|
||||
|
||||
MyMaze(final int width, final int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.partOfMaze = new boolean[this.width][this.height];
|
||||
for (int x = 0; x < this.width; x++) {
|
||||
this.partOfMaze[x] = new boolean[this.height];
|
||||
}
|
||||
this.completeColumns = new boolean[this.width];
|
||||
}
|
||||
|
||||
boolean isPartOfMaze(final int x, final int y) {
|
||||
return this.partOfMaze[x][y];
|
||||
}
|
||||
|
||||
boolean isPartOfMaze(@NotNull final Position position) {
|
||||
return this.isPartOfMaze(position.x(), position.y());
|
||||
}
|
||||
|
||||
void setPartOfMaze(final int x, final int y) {
|
||||
this.partOfMaze[x][y] = true;
|
||||
this.checkCompleteColumn(x);
|
||||
}
|
||||
|
||||
void setPartOfMaze(@NotNull final Position position) {
|
||||
this.setPartOfMaze(position.x(), position.y());
|
||||
}
|
||||
|
||||
void setPartOfMaze(@NotNull final Path path) {
|
||||
path.path.forEach(this::setPartOfMaze);
|
||||
}
|
||||
|
||||
void checkCompleteColumn(final int x) {
|
||||
if (this.completeColumns[x]) {
|
||||
return;
|
||||
}
|
||||
for (int y = 0; y < this.height; y++) {
|
||||
if (!this.isPartOfMaze(x, y)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.completeColumns[x] = true;
|
||||
}
|
||||
|
||||
Option<Position> getRandomAvailablePosition(@NotNull final Random random) {
|
||||
final Seq<Integer> allowedColumns = Vector.ofAll(this.completeColumns)
|
||||
.zipWithIndex()
|
||||
.reject(Tuple2::_1)
|
||||
.map(Tuple2::_2);
|
||||
if (allowedColumns.isEmpty()) {
|
||||
return Option.none();
|
||||
}
|
||||
final int x = allowedColumns.get(random.nextInt(allowedColumns.size()));
|
||||
final boolean[] column = partOfMaze[x];
|
||||
|
||||
final Seq<Integer> allowedRows = Vector.ofAll(column)
|
||||
.zipWithIndex()
|
||||
.reject(Tuple2::_1)
|
||||
.map(Tuple2::_2);
|
||||
if (allowedRows.isEmpty()) {
|
||||
return Option.none();
|
||||
}
|
||||
final int y = allowedRows.get(random.nextInt(allowedRows.size()));
|
||||
return Option.some(new Position(x, y));
|
||||
}
|
||||
|
||||
public Path createPath(@NotNull final Position position) {
|
||||
return new Path(position);
|
||||
}
|
||||
|
||||
private class Path {
|
||||
@NotNull
|
||||
private final List<Position> path = new LinkedList<>();
|
||||
|
||||
Path(@NotNull final Position position) {
|
||||
this.path.add(position);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Position nextRandomPosition(@NotNull final Random random) {
|
||||
final Direction direction = this.getRandomDirectionFromLastPosition(random);
|
||||
return this.path.getLast().move(direction);
|
||||
}
|
||||
|
||||
boolean contains(@NotNull final Position position) {
|
||||
return this.path.contains(position);
|
||||
}
|
||||
|
||||
void removeLoopUpTo(@NotNull final Position position) {
|
||||
while (!this.path.removeLast().equals(position)) {
|
||||
}
|
||||
this.path.add(position);
|
||||
}
|
||||
|
||||
public void append(@NotNull final Position nextPosition) {
|
||||
this.path.add(nextPosition);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Direction getRandomDirectionFromLastPosition(@NotNull final Random random) {
|
||||
final EnumSet<Direction> validDirections = this.getValidDirectionsFromLastPosition();
|
||||
if (validDirections.isEmpty()) {
|
||||
throw new IllegalStateException("WE MUST NOT GET HERE! analyze why it happened!!!");
|
||||
}
|
||||
if (validDirections.size() == 1) {
|
||||
return validDirections.iterator().next();
|
||||
}
|
||||
final Direction[] directionArray = validDirections.toArray(Direction[]::new);
|
||||
final int index = random.nextInt(directionArray.length);
|
||||
return directionArray[index];
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private EnumSet<Direction> getValidDirectionsFromLastPosition() {
|
||||
final Position fromPosition = this.path.getLast();
|
||||
final EnumSet<Direction> validDirections = EnumSet.allOf(Direction.class);
|
||||
if (this.path.size() > 1) {
|
||||
final Position prevP = this.path.get(this.path.size() - 2);
|
||||
fromPosition.getDirectionTo(prevP)
|
||||
.forEach(validDirections::remove);
|
||||
}
|
||||
boolean canLeft = fromPosition.x() > 0;
|
||||
boolean canRight = fromPosition.x() < width - 1;
|
||||
boolean canUp = fromPosition.y() > 0;
|
||||
boolean canDown = fromPosition.y() < height - 1;
|
||||
if (!canLeft) {
|
||||
validDirections.remove(Direction.LEFT);
|
||||
}
|
||||
if (!canRight) {
|
||||
validDirections.remove(Direction.RIGHT);
|
||||
}
|
||||
if (!canUp) {
|
||||
validDirections.remove(Direction.TOP);
|
||||
}
|
||||
if (!canDown) {
|
||||
validDirections.remove(Direction.BOTTOM);
|
||||
}
|
||||
return validDirections;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Stream.ofAll(this.path)
|
||||
.map(position -> "(%s,%s)".formatted(position.x(), position.y()))
|
||||
.mkString("Path[", "->", "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,14 @@
|
|||
package ch.fritteli.maze.generator.model;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public enum Direction {
|
||||
TOP,
|
||||
RIGHT,
|
||||
BOTTOM,
|
||||
LEFT;
|
||||
|
||||
@NotNull
|
||||
public Direction invert() {
|
||||
return switch (this) {
|
||||
case TOP -> BOTTOM;
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package ch.fritteli.maze.generator.model;
|
||||
|
||||
import io.vavr.control.Option;
|
||||
import lombok.With;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@With
|
||||
public record Position(int x, int y) {
|
||||
@NotNull
|
||||
public Position move(@NotNull final Direction direction) {
|
||||
return switch (direction) {
|
||||
case BOTTOM -> this.withY(this.y + 1);
|
||||
|
@ -13,4 +15,20 @@ public record Position(int x, int y) {
|
|||
case TOP -> this.withY(this.y - 1);
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Option<Direction> getDirectionTo(@NotNull final Position position) {
|
||||
final int xDiff = position.x - this.x;
|
||||
final int yDiff = position.y - this.y;
|
||||
return switch (xDiff) {
|
||||
case -1 -> Option.when(yDiff == 0, Direction.LEFT);
|
||||
case 0 -> switch (yDiff) {
|
||||
case -1 -> Option.some(Direction.TOP);
|
||||
case 1 -> Option.some(Direction.BOTTOM);
|
||||
default -> Option.none();
|
||||
};
|
||||
case 1 -> Option.when(yDiff == 0, Direction.RIGHT);
|
||||
default -> Option.none();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -233,7 +233,7 @@ class Generator {
|
|||
if (position.equals(previousPosition)) {
|
||||
continue;
|
||||
}
|
||||
if (tileAtPosition.map(Tile::isSolution).getOrElse(false)) {
|
||||
if (tileAtPosition.exists(Tile::isSolution)) {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package ch.fritteli.maze.generator.algorithm;
|
||||
|
||||
import ch.fritteli.maze.generator.model.Maze;
|
||||
import ch.fritteli.maze.generator.renderer.text.TextRenderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class WilsonTest {
|
||||
@Test
|
||||
void foo() {
|
||||
final Maze maze = new Maze(50, 10, 0);
|
||||
final Wilson wilson = new Wilson(maze);
|
||||
wilson.run();
|
||||
System.out.println(TextRenderer.newInstance().render(maze));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue