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.Deque;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class RandomDepthFirst {
|
public class RandomDepthFirst extends AbstractMazeGeneratorAlgorithm {
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private final Maze maze;
|
|
||||||
@NotNull
|
|
||||||
private final Random random;
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private final Deque<Position> positions = new LinkedList<>();
|
private final Deque<Position> positions = new LinkedList<>();
|
||||||
|
|
||||||
public RandomDepthFirst(@NotNull final Maze maze) {
|
public RandomDepthFirst(@NotNull final Maze maze) {
|
||||||
this.maze = maze;
|
super(maze);
|
||||||
this.random = new Random(maze.getRandomSeed());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void run() {
|
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;
|
package ch.fritteli.maze.generator.model;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public enum Direction {
|
public enum Direction {
|
||||||
TOP,
|
TOP,
|
||||||
RIGHT,
|
RIGHT,
|
||||||
BOTTOM,
|
BOTTOM,
|
||||||
LEFT;
|
LEFT;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
public Direction invert() {
|
public Direction invert() {
|
||||||
return switch (this) {
|
return switch (this) {
|
||||||
case TOP -> BOTTOM;
|
case TOP -> BOTTOM;
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package ch.fritteli.maze.generator.model;
|
package ch.fritteli.maze.generator.model;
|
||||||
|
|
||||||
|
import io.vavr.control.Option;
|
||||||
import lombok.With;
|
import lombok.With;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@With
|
@With
|
||||||
public record Position(int x, int y) {
|
public record Position(int x, int y) {
|
||||||
|
@NotNull
|
||||||
public Position move(@NotNull final Direction direction) {
|
public Position move(@NotNull final Direction direction) {
|
||||||
return switch (direction) {
|
return switch (direction) {
|
||||||
case BOTTOM -> this.withY(this.y + 1);
|
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);
|
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)) {
|
if (position.equals(previousPosition)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (tileAtPosition.map(Tile::isSolution).getOrElse(false)) {
|
if (tileAtPosition.exists(Tile::isSolution)) {
|
||||||
return position;
|
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