Refactoring and clean-up. It is better organized, but the very basic data structures
of the maze are not really suited for Wilson's algorithm. Another refactoring will be required.
This commit is contained in:
parent
9599be21b3
commit
5e6965fa55
23 changed files with 557 additions and 396 deletions
5
pom.xml
5
pom.xml
|
@ -74,6 +74,11 @@
|
||||||
<groupId>io.vavr</groupId>
|
<groupId>io.vavr</groupId>
|
||||||
<artifactId>vavr</artifactId>
|
<artifactId>vavr</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
<version>33.2.1-jre</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.pdfbox</groupId>
|
<groupId>org.apache.pdfbox</groupId>
|
||||||
<artifactId>pdfbox</artifactId>
|
<artifactId>pdfbox</artifactId>
|
||||||
|
|
|
@ -1,312 +0,0 @@
|
||||||
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, "Wilson");
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
// seal all walls, mark all as visited
|
|
||||||
for (int x = 0; x < maze.getWidth(); x++) {
|
|
||||||
for (int y = 0; y < maze.getHeight(); y++) {
|
|
||||||
maze.getTileAt(x, y).forEach(tile -> {
|
|
||||||
Stream.of(Direction.values())
|
|
||||||
.forEach(d -> {
|
|
||||||
if (tile.hasWallAt(d)) {
|
|
||||||
tile.preventDiggingToOrFrom(d);
|
|
||||||
} else {
|
|
||||||
tile.digFrom(d);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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[", "->", "]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
package ch.fritteli.maze.generator.algorithm.wilson;
|
||||||
|
|
||||||
|
import ch.fritteli.maze.generator.model.Direction;
|
||||||
|
import ch.fritteli.maze.generator.model.Maze;
|
||||||
|
import ch.fritteli.maze.generator.model.Position;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Stack;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class MazeSolver {
|
||||||
|
@NotNull
|
||||||
|
private final Maze maze;
|
||||||
|
|
||||||
|
MazeSolver(@NotNull final Maze maze) {
|
||||||
|
this.maze = maze;
|
||||||
|
}
|
||||||
|
|
||||||
|
void solve() {
|
||||||
|
final Direction directionToOuterWall = Wilson.getDirectionToOuterWall(
|
||||||
|
this.maze.getStart(),
|
||||||
|
this.maze.getWidth(),
|
||||||
|
this.maze.getHeight()
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<Position> solution = this.getSolution(this.maze.getStart(), directionToOuterWall);
|
||||||
|
for (Position position : solution) {
|
||||||
|
this.maze.getTileAt(position).get().setSolution();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Position> getSolution(@NotNull Position position,
|
||||||
|
@NotNull Direction forbidden) {
|
||||||
|
record PathElement(@NotNull Position position,
|
||||||
|
@NotNull EnumSet<Direction> possibleDirections) {
|
||||||
|
}
|
||||||
|
final Stack<PathElement> solution = new Stack<>();
|
||||||
|
final EnumSet<Direction> directions = this.maze.getTileAt(position).get().getOpenDirections();
|
||||||
|
directions.remove(forbidden);
|
||||||
|
PathElement head = new PathElement(position, directions);
|
||||||
|
solution.push(head);
|
||||||
|
while (!head.position.equals(this.maze.getEnd())) {
|
||||||
|
if (head.possibleDirections.isEmpty()) {
|
||||||
|
solution.pop();
|
||||||
|
head = solution.peek();
|
||||||
|
} else {
|
||||||
|
final Iterator<Direction> iterator = head.possibleDirections.iterator();
|
||||||
|
final Direction direction = iterator.next();
|
||||||
|
iterator.remove();
|
||||||
|
final Position next = head.position.move(direction);
|
||||||
|
final EnumSet<Direction> openDirections = this.maze.getTileAt(next).get().getOpenDirections();
|
||||||
|
openDirections.remove(direction.invert());
|
||||||
|
head = new PathElement(next, openDirections);
|
||||||
|
solution.push(head);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return solution.stream().map(PathElement::position).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package ch.fritteli.maze.generator.algorithm.wilson;
|
||||||
|
|
||||||
|
import ch.fritteli.maze.generator.model.Direction;
|
||||||
|
import ch.fritteli.maze.generator.model.Position;
|
||||||
|
import io.vavr.collection.List;
|
||||||
|
import io.vavr.collection.Stream;
|
||||||
|
import io.vavr.collection.Traversable;
|
||||||
|
import io.vavr.control.Option;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
class Path {
|
||||||
|
private final int width;
|
||||||
|
private final int height;
|
||||||
|
@NotNull
|
||||||
|
private List<Position> positions;
|
||||||
|
|
||||||
|
Path(@NotNull final Position start, int width, int height) {
|
||||||
|
this.positions = List.of(start);
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
Position growRandom(@NotNull final Random random) {
|
||||||
|
final Position position = this.nextRandomPosition(random);
|
||||||
|
if (this.contains(position)) {
|
||||||
|
this.removeLoopUpTo(position);
|
||||||
|
return this.growRandom(random);
|
||||||
|
}
|
||||||
|
this.positions = this.positions.prepend(position);
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
List<Position> getPositions() {
|
||||||
|
return this.positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
Position getStart() {
|
||||||
|
return this.positions.last();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
Traversable<Direction> getMovesFromStart() {
|
||||||
|
return this.positions.reverse().sliding(2)
|
||||||
|
.flatMap(positions1 -> Option.when(
|
||||||
|
positions1.size() == 2,
|
||||||
|
// DEV-NOTE: .get() is safe here, because in the context of a path, there MUST be a direction
|
||||||
|
// from one position to the next.
|
||||||
|
() -> positions1.head().getDirectionTo(positions1.last()).get()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Position nextRandomPosition(@NotNull final Random random) {
|
||||||
|
final Direction randomDirection = this.getRandomDirection(random);
|
||||||
|
final Position nextPosition = this.positions.head().move(randomDirection);
|
||||||
|
if (this.isWithinBounds(nextPosition) && !nextPosition.equals(this.positions.head())) {
|
||||||
|
return nextPosition;
|
||||||
|
}
|
||||||
|
return this.nextRandomPosition(random);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWithinBounds(@NotNull final Position position) {
|
||||||
|
return position.x() >= 0 && position.x() < this.width && position.y() >= 0 && position.y() < this.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean contains(@NotNull final Position position) {
|
||||||
|
return this.positions.contains(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeLoopUpTo(@NotNull final Position position) {
|
||||||
|
this.positions = this.positions.dropUntil(position::equals);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Direction getRandomDirection(@NotNull final Random random) {
|
||||||
|
final Direction[] array = Direction.values();
|
||||||
|
return array[random.nextInt(array.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return Stream.ofAll(this.positions)
|
||||||
|
.map(position -> "(%s,%s)".formatted(position.x(), position.y()))
|
||||||
|
.mkString("Path[", "->", "]");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
package ch.fritteli.maze.generator.algorithm.wilson;
|
||||||
|
|
||||||
|
import ch.fritteli.maze.generator.model.Maze;
|
||||||
|
import ch.fritteli.maze.generator.model.Position;
|
||||||
|
import com.google.common.collect.HashMultimap;
|
||||||
|
import com.google.common.collect.Multimap;
|
||||||
|
import io.vavr.Tuple;
|
||||||
|
import io.vavr.collection.Stream;
|
||||||
|
import io.vavr.collection.Traversable;
|
||||||
|
import io.vavr.control.Option;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class will build paths such that in the end all fields of the maze are covered by exactly one path.
|
||||||
|
*/
|
||||||
|
class PathsBuilder {
|
||||||
|
private final int width;
|
||||||
|
private final int height;
|
||||||
|
@NotNull
|
||||||
|
private final Random random;
|
||||||
|
@NotNull
|
||||||
|
private final Multimap<Integer, Integer> availablePositions;
|
||||||
|
|
||||||
|
PathsBuilder(@NotNull final Maze maze,
|
||||||
|
@NotNull final Random random) {
|
||||||
|
this.width = maze.getWidth();
|
||||||
|
this.height = maze.getHeight();
|
||||||
|
this.random = random;
|
||||||
|
this.availablePositions = HashMultimap.create(this.width, this.height);
|
||||||
|
|
||||||
|
// Initialize the available positions.
|
||||||
|
for (int x = 0; x < this.width; x++) {
|
||||||
|
for (int y = 0; y < this.height; y++) {
|
||||||
|
this.availablePositions.put(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create all the paths such that the maze will be completely filled and every field of it will be covered by
|
||||||
|
* exactly one path.
|
||||||
|
*
|
||||||
|
* @return A {@link Traversable} of generated {@link Path Paths}.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
Traversable<Path> buildPaths() {
|
||||||
|
this.initializeWithRandomStartingPosition();
|
||||||
|
|
||||||
|
return Stream.unfoldLeft(
|
||||||
|
this,
|
||||||
|
builder -> builder.buildPath()
|
||||||
|
.map(path -> {
|
||||||
|
builder.setPartOfMaze(path);
|
||||||
|
return Tuple.of(builder, path);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeWithRandomStartingPosition() {
|
||||||
|
this.popRandomPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates one new path, if possible. If the maze is already filled, {@link io.vavr.control.Option.None} is
|
||||||
|
* returned.
|
||||||
|
*
|
||||||
|
* @return An {@link Option} of a new {@link Path} instance.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private Option<Path> buildPath() {
|
||||||
|
return this.initializeNewPath()
|
||||||
|
.map(this::growPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Option<Path> initializeNewPath() {
|
||||||
|
return this.popRandomPosition()
|
||||||
|
.map(position -> new Path(position, this.width, this.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Randomly grow the {@code path} until it reaches a field that is part of the maze and return it. The resulting
|
||||||
|
* path will contain no loops.
|
||||||
|
*
|
||||||
|
* @param path The {@link Path} to grow.
|
||||||
|
* @return The final {@link Path} that reaches the maze.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private Path growPath(@NotNull final Path path) {
|
||||||
|
Position lastPosition;
|
||||||
|
do {
|
||||||
|
lastPosition = path.growRandom(this.random);
|
||||||
|
} while (this.isNotPartOfMaze(lastPosition));
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isNotPartOfMaze(@NotNull final Position position) {
|
||||||
|
return this.availablePositions.containsEntry(position.x(), position.y());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPartOfMaze(@NotNull final Position position) {
|
||||||
|
this.availablePositions.remove(position.x(), position.y());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPartOfMaze(@NotNull final Path path) {
|
||||||
|
path.getPositions().forEach(this::setPartOfMaze);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a random {@link Position}, that is not yet part of the maze, marks it as being part of the maze and returns
|
||||||
|
* it. If no position is available, {@link io.vavr.control.Option.None} is returned.
|
||||||
|
*
|
||||||
|
* @return An available position or {@link io.vavr.control.Option.None}.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private Option<Position> popRandomPosition() {
|
||||||
|
if (this.availablePositions.isEmpty()) {
|
||||||
|
return Option.none();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Integer[] keys = this.availablePositions.keySet().toArray(Integer[]::new);
|
||||||
|
final int key = keys[this.random.nextInt(keys.length)];
|
||||||
|
final Integer[] values = this.availablePositions.get(key).toArray(Integer[]::new);
|
||||||
|
final int value = values[this.random.nextInt(values.length)];
|
||||||
|
|
||||||
|
this.availablePositions.remove(key, value);
|
||||||
|
return Option.some(new Position(key, value));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package ch.fritteli.maze.generator.algorithm.wilson;
|
||||||
|
|
||||||
|
import ch.fritteli.maze.generator.algorithm.AbstractMazeGeneratorAlgorithm;
|
||||||
|
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.collection.Traversable;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of <a href="https://en.wikipedia.org/wiki/Maze_generation_algorithm#Wilson's_algorithm">Wilson's Algorithm</a>.
|
||||||
|
* In short:
|
||||||
|
* <ol>
|
||||||
|
* <li>Pick random location, add to maze</li>
|
||||||
|
* <li>While locations that are not part of the maze exist, loop:
|
||||||
|
* <ol>
|
||||||
|
* <li>Pick random location that's not part of the maze</li>
|
||||||
|
* <li>Randomly walk from this location, until ...
|
||||||
|
* <ul>
|
||||||
|
* <li>... either you hit the current path, forming a loop. Then remove the entire loop and continue
|
||||||
|
* walking.</li>
|
||||||
|
* <li>... or you hit a position that is part of the maze. Then add the path to the maze and start the next
|
||||||
|
* walk.</li>
|
||||||
|
* </ul></li>
|
||||||
|
* </ol></li>
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
public class Wilson extends AbstractMazeGeneratorAlgorithm {
|
||||||
|
|
||||||
|
public Wilson(@NotNull final Maze maze) {
|
||||||
|
super(maze, "Wilson");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static Direction getDirectionToOuterWall(@NotNull final Position position,
|
||||||
|
final int width,
|
||||||
|
final int height) {
|
||||||
|
if (position.y() == 0) {
|
||||||
|
return Direction.TOP;
|
||||||
|
}
|
||||||
|
if (position.y() == height - 1) {
|
||||||
|
return Direction.BOTTOM;
|
||||||
|
}
|
||||||
|
if (position.x() == 0) {
|
||||||
|
return Direction.LEFT;
|
||||||
|
}
|
||||||
|
if (position.x() == width - 1) {
|
||||||
|
return Direction.RIGHT;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
final Traversable<Path> paths = new PathsBuilder(this.maze, this.random)
|
||||||
|
.buildPaths();
|
||||||
|
|
||||||
|
this.applyPathsToMaze(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPathsToMaze(@NotNull final Traversable<Path> paths) {
|
||||||
|
this.openStartAndEndWalls();
|
||||||
|
paths.forEach(path -> path.getMovesFromStart()
|
||||||
|
.foldLeft(
|
||||||
|
path.getStart(),
|
||||||
|
(position, direction) -> {
|
||||||
|
this.maze.getTileAt(position).get()
|
||||||
|
.digTo(direction);
|
||||||
|
final Position next = position.move(direction);
|
||||||
|
this.maze.getTileAt(next).get()
|
||||||
|
.digTo(direction.invert());
|
||||||
|
return next;
|
||||||
|
}));
|
||||||
|
|
||||||
|
final MazeSolver solver = new MazeSolver(this.maze);
|
||||||
|
solver.solve();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openStartAndEndWalls() {
|
||||||
|
this.openWall(this.maze.getStart(), this.maze.getStartTile());
|
||||||
|
this.openWall(this.maze.getEnd(), this.maze.getEndTile());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openWall(@NotNull final Position position, @NotNull final Tile tile) {
|
||||||
|
final Direction direction = this.getDirectionToOuterWall(position);
|
||||||
|
tile.enableDiggingToOrFrom(direction);
|
||||||
|
tile.digTo(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Direction getDirectionToOuterWall(@NotNull final Position position) {
|
||||||
|
return getDirectionToOuterWall(position, this.maze.getWidth(), this.maze.getHeight());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package ch.fritteli.maze.generator.model;
|
package ch.fritteli.maze.generator.model;
|
||||||
|
|
||||||
import io.vavr.collection.Stream;
|
import io.vavr.collection.Vector;
|
||||||
import io.vavr.control.Option;
|
import io.vavr.control.Option;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
@ -17,6 +17,7 @@ import java.util.Random;
|
||||||
@ToString
|
@ToString
|
||||||
public class Tile {
|
public class Tile {
|
||||||
final Walls walls = new Walls();
|
final Walls walls = new Walls();
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
boolean visited = false;
|
boolean visited = false;
|
||||||
@Getter
|
@Getter
|
||||||
boolean solution = false;
|
boolean solution = false;
|
||||||
|
@ -65,13 +66,23 @@ public class Tile {
|
||||||
this.walls.set(direction);
|
this.walls.set(direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public EnumSet<Direction> getOpenDirections() {
|
||||||
|
return this.walls.getOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
public Option<Direction> getRandomAvailableDirection(@NotNull final Random random) {
|
public Option<Direction> getRandomAvailableDirection(@NotNull final Random random) {
|
||||||
final Stream<Direction> availableDirections = this.walls.getUnsealedSet();
|
final EnumSet<Direction> availableDirections = this.walls.getUnsealedSet();
|
||||||
if (availableDirections.isEmpty()) {
|
if (availableDirections.isEmpty()) {
|
||||||
return Option.none();
|
return Option.none();
|
||||||
}
|
}
|
||||||
final int index = random.nextInt(availableDirections.length());
|
if (availableDirections.size() == 1) {
|
||||||
return Option.of(availableDirections.get(index));
|
return Option.some(availableDirections.iterator().next());
|
||||||
|
}
|
||||||
|
final Vector<Direction> directions = Vector.ofAll(availableDirections);
|
||||||
|
final int index = random.nextInt(directions.size());
|
||||||
|
return Option.of(directions.get(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasWallAt(@NotNull final Direction direction) {
|
public boolean hasWallAt(@NotNull final Direction direction) {
|
||||||
|
|
|
@ -1,21 +1,17 @@
|
||||||
package ch.fritteli.maze.generator.model;
|
package ch.fritteli.maze.generator.model;
|
||||||
|
|
||||||
import io.vavr.collection.Stream;
|
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.SortedSet;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
|
|
||||||
@EqualsAndHashCode
|
@EqualsAndHashCode
|
||||||
@ToString
|
@ToString
|
||||||
public class Walls {
|
public class Walls {
|
||||||
private final SortedSet<Direction> directions = new TreeSet<>();
|
private final EnumSet<Direction> directions = EnumSet.noneOf(Direction.class);
|
||||||
private final Set<Direction> sealed = new HashSet<>();
|
@EqualsAndHashCode.Exclude
|
||||||
|
private final EnumSet<Direction> sealed = EnumSet.noneOf(Direction.class);
|
||||||
|
|
||||||
public void set(@NotNull final Direction direction) {
|
public void set(@NotNull final Direction direction) {
|
||||||
this.directions.add(direction);
|
this.directions.add(direction);
|
||||||
|
@ -36,9 +32,11 @@ public class Walls {
|
||||||
return this.directions.contains(direction);
|
return this.directions.contains(direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream<Direction> getUnsealedSet() {
|
@NotNull
|
||||||
return Stream.ofAll(this.directions)
|
public EnumSet<Direction> getUnsealedSet() {
|
||||||
.removeAll(this.sealed);
|
final EnumSet<Direction> result = EnumSet.copyOf(this.directions);
|
||||||
|
result.removeAll(this.sealed);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void seal(@NotNull final Direction direction) {
|
public void seal(@NotNull final Direction direction) {
|
||||||
|
@ -51,4 +49,9 @@ public class Walls {
|
||||||
public void unseal(@NotNull final Direction direction) {
|
public void unseal(@NotNull final Direction direction) {
|
||||||
this.sealed.remove(direction);
|
this.sealed.remove(direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public EnumSet<Direction> getOpen() {
|
||||||
|
return EnumSet.complementOf(this.directions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package ch.fritteli.maze.generator.serialization;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class MazeConstants {
|
||||||
|
public final byte MAGIC_BYTE_1 = 0x1a;
|
||||||
|
public final byte MAGIC_BYTE_2 = (byte) 0xb1;
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze;
|
||||||
import ch.fritteli.maze.generator.model.Tile;
|
import ch.fritteli.maze.generator.model.Tile;
|
||||||
import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
|
import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
|
||||||
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
||||||
|
import ch.fritteli.maze.generator.serialization.MazeConstants;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class MazeInputStreamV1 extends AbstractMazeInputStream {
|
public class MazeInputStreamV1 extends AbstractMazeInputStream {
|
||||||
|
@ -18,11 +19,11 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream {
|
||||||
// 01 0xb1 magic
|
// 01 0xb1 magic
|
||||||
// 02 0x01 version
|
// 02 0x01 version
|
||||||
final byte magic1 = this.readByte();
|
final byte magic1 = this.readByte();
|
||||||
if (magic1 != SerializerDeserializerV1.MAGIC_BYTE_1) {
|
if (magic1 != MazeConstants.MAGIC_BYTE_1) {
|
||||||
throw new IllegalArgumentException("Invalid maze data.");
|
throw new IllegalArgumentException("Invalid maze data.");
|
||||||
}
|
}
|
||||||
final byte magic2 = this.readByte();
|
final byte magic2 = this.readByte();
|
||||||
if (magic2 != SerializerDeserializerV1.MAGIC_BYTE_2) {
|
if (magic2 != MazeConstants.MAGIC_BYTE_2) {
|
||||||
throw new IllegalArgumentException("Invalid maze data.");
|
throw new IllegalArgumentException("Invalid maze data.");
|
||||||
}
|
}
|
||||||
final int version = this.readByte();
|
final int version = this.readByte();
|
||||||
|
|
|
@ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze;
|
||||||
import ch.fritteli.maze.generator.model.Tile;
|
import ch.fritteli.maze.generator.model.Tile;
|
||||||
import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
|
import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
|
||||||
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
||||||
|
import ch.fritteli.maze.generator.serialization.MazeConstants;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class MazeOutputStreamV1 extends AbstractMazeOutputStream {
|
public class MazeOutputStreamV1 extends AbstractMazeOutputStream {
|
||||||
|
@ -13,8 +14,8 @@ public class MazeOutputStreamV1 extends AbstractMazeOutputStream {
|
||||||
// 00 0x1a magic
|
// 00 0x1a magic
|
||||||
// 01 0xb1 magic
|
// 01 0xb1 magic
|
||||||
// 02 0x02 version
|
// 02 0x02 version
|
||||||
this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_1);
|
this.writeByte(MazeConstants.MAGIC_BYTE_1);
|
||||||
this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_2);
|
this.writeByte(MazeConstants.MAGIC_BYTE_2);
|
||||||
this.writeByte(SerializerDeserializerV1.VERSION_BYTE);
|
this.writeByte(SerializerDeserializerV1.VERSION_BYTE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,9 +24,7 @@ import java.lang.reflect.InvocationTargetException;
|
||||||
*/
|
*/
|
||||||
@UtilityClass
|
@UtilityClass
|
||||||
public class SerializerDeserializerV1 {
|
public class SerializerDeserializerV1 {
|
||||||
final byte MAGIC_BYTE_1 = 0x1a;
|
public final byte VERSION_BYTE = 0x01;
|
||||||
final byte MAGIC_BYTE_2 = (byte) 0xb1;
|
|
||||||
final byte VERSION_BYTE = 0x01;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes the {@code maze} into a byte array.
|
* Serializes the {@code maze} into a byte array.
|
||||||
|
|
|
@ -5,6 +5,7 @@ import ch.fritteli.maze.generator.model.Position;
|
||||||
import ch.fritteli.maze.generator.model.Tile;
|
import ch.fritteli.maze.generator.model.Tile;
|
||||||
import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
|
import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
|
||||||
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
||||||
|
import ch.fritteli.maze.generator.serialization.MazeConstants;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class MazeInputStreamV2 extends AbstractMazeInputStream {
|
public class MazeInputStreamV2 extends AbstractMazeInputStream {
|
||||||
|
@ -19,11 +20,11 @@ public class MazeInputStreamV2 extends AbstractMazeInputStream {
|
||||||
// 01 0xb1 magic
|
// 01 0xb1 magic
|
||||||
// 02 0x02 version
|
// 02 0x02 version
|
||||||
final byte magic1 = this.readByte();
|
final byte magic1 = this.readByte();
|
||||||
if (magic1 != SerializerDeserializerV2.MAGIC_BYTE_1) {
|
if (magic1 != MazeConstants.MAGIC_BYTE_1) {
|
||||||
throw new IllegalArgumentException("Invalid maze data.");
|
throw new IllegalArgumentException("Invalid maze data.");
|
||||||
}
|
}
|
||||||
final byte magic2 = this.readByte();
|
final byte magic2 = this.readByte();
|
||||||
if (magic2 != SerializerDeserializerV2.MAGIC_BYTE_2) {
|
if (magic2 != MazeConstants.MAGIC_BYTE_2) {
|
||||||
throw new IllegalArgumentException("Invalid maze data.");
|
throw new IllegalArgumentException("Invalid maze data.");
|
||||||
}
|
}
|
||||||
final int version = this.readByte();
|
final int version = this.readByte();
|
||||||
|
|
|
@ -5,6 +5,7 @@ import ch.fritteli.maze.generator.model.Position;
|
||||||
import ch.fritteli.maze.generator.model.Tile;
|
import ch.fritteli.maze.generator.model.Tile;
|
||||||
import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
|
import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
|
||||||
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
||||||
|
import ch.fritteli.maze.generator.serialization.MazeConstants;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class MazeOutputStreamV2 extends AbstractMazeOutputStream {
|
public class MazeOutputStreamV2 extends AbstractMazeOutputStream {
|
||||||
|
@ -14,8 +15,8 @@ public class MazeOutputStreamV2 extends AbstractMazeOutputStream {
|
||||||
// 00 0x1a magic
|
// 00 0x1a magic
|
||||||
// 01 0xb1 magic
|
// 01 0xb1 magic
|
||||||
// 02 0x02 version
|
// 02 0x02 version
|
||||||
this.writeByte(SerializerDeserializerV2.MAGIC_BYTE_1);
|
this.writeByte(MazeConstants.MAGIC_BYTE_1);
|
||||||
this.writeByte(SerializerDeserializerV2.MAGIC_BYTE_2);
|
this.writeByte(MazeConstants.MAGIC_BYTE_2);
|
||||||
this.writeByte(SerializerDeserializerV2.VERSION_BYTE);
|
this.writeByte(SerializerDeserializerV2.VERSION_BYTE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,10 +29,7 @@ import java.lang.reflect.InvocationTargetException;
|
||||||
*/
|
*/
|
||||||
@UtilityClass
|
@UtilityClass
|
||||||
public class SerializerDeserializerV2 {
|
public class SerializerDeserializerV2 {
|
||||||
|
public final byte VERSION_BYTE = 0x02;
|
||||||
final byte MAGIC_BYTE_1 = 0x1a;
|
|
||||||
final byte MAGIC_BYTE_2 = (byte) 0xb1;
|
|
||||||
final byte VERSION_BYTE = 0x02;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes the {@code maze} into a byte array.
|
* Serializes the {@code maze} into a byte array.
|
||||||
|
|
|
@ -5,6 +5,7 @@ import ch.fritteli.maze.generator.model.Position;
|
||||||
import ch.fritteli.maze.generator.model.Tile;
|
import ch.fritteli.maze.generator.model.Tile;
|
||||||
import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
|
import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
|
||||||
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
||||||
|
import ch.fritteli.maze.generator.serialization.MazeConstants;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -22,11 +23,11 @@ public class MazeInputStreamV3 extends AbstractMazeInputStream {
|
||||||
// 01 0xb1 magic
|
// 01 0xb1 magic
|
||||||
// 02 0x03 version
|
// 02 0x03 version
|
||||||
final byte magic1 = this.readByte();
|
final byte magic1 = this.readByte();
|
||||||
if (magic1 != SerializerDeserializerV3.MAGIC_BYTE_1) {
|
if (magic1 != MazeConstants.MAGIC_BYTE_1) {
|
||||||
throw new IllegalArgumentException("Invalid maze data.");
|
throw new IllegalArgumentException("Invalid maze data.");
|
||||||
}
|
}
|
||||||
final byte magic2 = this.readByte();
|
final byte magic2 = this.readByte();
|
||||||
if (magic2 != SerializerDeserializerV3.MAGIC_BYTE_2) {
|
if (magic2 != MazeConstants.MAGIC_BYTE_2) {
|
||||||
throw new IllegalArgumentException("Invalid maze data.");
|
throw new IllegalArgumentException("Invalid maze data.");
|
||||||
}
|
}
|
||||||
final int version = this.readByte();
|
final int version = this.readByte();
|
||||||
|
|
|
@ -5,6 +5,7 @@ import ch.fritteli.maze.generator.model.Position;
|
||||||
import ch.fritteli.maze.generator.model.Tile;
|
import ch.fritteli.maze.generator.model.Tile;
|
||||||
import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
|
import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
|
||||||
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
||||||
|
import ch.fritteli.maze.generator.serialization.MazeConstants;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
@ -16,8 +17,8 @@ public class MazeOutputStreamV3 extends AbstractMazeOutputStream {
|
||||||
// 00 0x1a magic
|
// 00 0x1a magic
|
||||||
// 01 0xb1 magic
|
// 01 0xb1 magic
|
||||||
// 02 0x03 version
|
// 02 0x03 version
|
||||||
this.writeByte(SerializerDeserializerV3.MAGIC_BYTE_1);
|
this.writeByte(MazeConstants.MAGIC_BYTE_1);
|
||||||
this.writeByte(SerializerDeserializerV3.MAGIC_BYTE_2);
|
this.writeByte(MazeConstants.MAGIC_BYTE_2);
|
||||||
this.writeByte(SerializerDeserializerV3.VERSION_BYTE);
|
this.writeByte(SerializerDeserializerV3.VERSION_BYTE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,10 +32,7 @@ import java.lang.reflect.InvocationTargetException;
|
||||||
*/
|
*/
|
||||||
@UtilityClass
|
@UtilityClass
|
||||||
public class SerializerDeserializerV3 {
|
public class SerializerDeserializerV3 {
|
||||||
|
public final byte VERSION_BYTE = 0x03;
|
||||||
final byte MAGIC_BYTE_1 = 0x1a;
|
|
||||||
final byte MAGIC_BYTE_2 = (byte) 0xb1;
|
|
||||||
final byte VERSION_BYTE = 0x03;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes the {@code maze} into a byte array.
|
* Serializes the {@code maze} into a byte array.
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class WilsonTest {
|
|
||||||
@Test
|
|
||||||
void foo() {
|
|
||||||
// arrange
|
|
||||||
final Maze maze = new Maze(10, 10, 0);
|
|
||||||
final Wilson wilson = new Wilson(maze);
|
|
||||||
|
|
||||||
// act
|
|
||||||
wilson.run();
|
|
||||||
|
|
||||||
// assert
|
|
||||||
final String textRepresentation = TextRenderer.newInstance().render(maze);
|
|
||||||
assertThat(textRepresentation).isEqualTo("""
|
|
||||||
╷ ╶─────┬─────┬───┬─┐
|
|
||||||
│ │ │ │ │
|
|
||||||
│ ╷ ╶─┬─┴─┬─╴ ╵ ╷ │ │
|
|
||||||
│ │ │ │ │ │ │
|
|
||||||
│ └─┐ │ ╶─┼─┬─┬─┘ │ │
|
|
||||||
│ │ │ │ │ │ │ │
|
|
||||||
│ ╷ │ └─╴ │ ╵ ╵ ╶─┘ │
|
|
||||||
│ │ │ │ │
|
|
||||||
├─┴─┘ ╷ ┌─┴───┐ ╷ ╶─┤
|
|
||||||
│ │ │ │ │ │
|
|
||||||
│ ╷ ┌─┘ └───╴ │ └─┬─┤
|
|
||||||
│ │ │ │ │ │
|
|
||||||
├─┴─┘ ┌───╴ ╷ └─┐ ╵ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ ╶─┬─┴─╴ ┌─┘ ╶─┘ ╶─┤
|
|
||||||
│ │ │ │
|
|
||||||
├─╴ ├─╴ ┌─┘ ╶─┐ ╶───┤
|
|
||||||
│ │ │ │ │
|
|
||||||
├───┴─╴ └─┐ ╶─┴───┐ │
|
|
||||||
│ │ │ │
|
|
||||||
└─────────┴───────┘ ╵""");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
package ch.fritteli.maze.generator.algorithm.wilson;
|
||||||
|
|
||||||
|
import ch.fritteli.maze.generator.model.Maze;
|
||||||
|
import ch.fritteli.maze.generator.renderer.text.TextRenderer;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class WilsonTest {
|
||||||
|
@Test
|
||||||
|
void testTinyGeneration() {
|
||||||
|
// arrange
|
||||||
|
final Maze maze = new Maze(3, 3, 0);
|
||||||
|
final Wilson wilson = new Wilson(maze);
|
||||||
|
|
||||||
|
// act
|
||||||
|
wilson.run();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
final String textRepresentation = TextRenderer.newInstance().render(maze);
|
||||||
|
assertThat(textRepresentation).isEqualTo("""
|
||||||
|
╷ ╶───┐
|
||||||
|
│ │
|
||||||
|
│ ┌─┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ╵ │ │
|
||||||
|
│ │ │
|
||||||
|
└───┘ ╵""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSimpleGeneration() {
|
||||||
|
// arrange
|
||||||
|
final Maze maze = new Maze(10, 10, 0);
|
||||||
|
final Wilson wilson = new Wilson(maze);
|
||||||
|
|
||||||
|
// act
|
||||||
|
wilson.run();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
final String textRepresentation = TextRenderer.newInstance().render(maze);
|
||||||
|
assertThat(textRepresentation).isEqualTo("""
|
||||||
|
╷ ╶─────┬─────┬─────┐
|
||||||
|
│ │ │ │
|
||||||
|
├─╴ ╷ ╶─┴─┬─╴ ├─┐ ╶─┤
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ ╷ └─┐ ╶─┘ ┌─┘ ╵ ╷ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ ╶─┤ ╶─┐ ╵ ╷ ╶─┤ │
|
||||||
|
│ │ │ │ │ │ │
|
||||||
|
│ ├─┐ ├─╴ └─┐ │ ╶─┼─┤
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
├─┘ └─┘ ╶─┐ └─┤ ╶─┤ │
|
||||||
|
│ │ │ │ │
|
||||||
|
├───┐ ╷ ╶─┴───┴─┐ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
├─╴ └─┼─╴ ╷ ╷ ┌─┴─┘ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ ╷ ╶─┴─┬─┤ ├─┘ ╶─┐ │
|
||||||
|
│ │ │ │ │ │ │
|
||||||
|
│ ├─╴ ╷ ╵ ╵ ╵ ┌─╴ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
└─┴───┴───────┴───┘ ╵""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSimpleGenerationWithSolution() {
|
||||||
|
// arrange
|
||||||
|
final Maze maze = new Maze(10, 10, 0);
|
||||||
|
final Wilson wilson = new Wilson(maze);
|
||||||
|
|
||||||
|
// act
|
||||||
|
wilson.run();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
final String textRepresentation = TextRenderer.newInstance().setRenderSolution(true).render(maze);
|
||||||
|
assertThat(textRepresentation).isEqualTo("""
|
||||||
|
╷│╶─────┬─────┬─────┐
|
||||||
|
│╰───╮ │ │ │
|
||||||
|
├─╴ ╷│╶─┴─┬─╴ ├─┐ ╶─┤
|
||||||
|
│ │╰─╮ │ │ │ │
|
||||||
|
│ ╷ └─┐│╶─┘ ┌─┘ ╵ ╷ │
|
||||||
|
│ │ ││ │ │ │
|
||||||
|
│ │ ╶─┤│╶─┐ ╵ ╷ ╶─┤ │
|
||||||
|
│ │ │╰─╮│ │ │ │
|
||||||
|
│ ├─┐ ├─╴│└─┐ │ ╶─┼─┤
|
||||||
|
│ │ │ │╭─╯ │ │ │ │
|
||||||
|
├─┘ └─┘│╶─┐ └─┤ ╶─┤ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
├───┐ ╷│╶─┴───┴─┐ │ │
|
||||||
|
│ │ │╰───╮ │ │ │
|
||||||
|
├─╴ └─┼─╴ ╷│╷ ┌─┴─┘ │
|
||||||
|
│ │ │││ │╭───╮│
|
||||||
|
│ ╷ ╶─┴─┬─┤│├─┘│╶─┐││
|
||||||
|
│ │ │ │││╭─╯ │││
|
||||||
|
│ ├─╴ ╷ ╵ ╵│╵│┌─╴ │││
|
||||||
|
│ │ │ ╰─╯│ │││
|
||||||
|
└─┴───┴───────┴───┘│╵""");
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ package ch.fritteli.maze.generator.model;
|
||||||
import io.vavr.collection.Stream;
|
import io.vavr.collection.Stream;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
|
||||||
|
@ -113,7 +115,7 @@ class WallsTest {
|
||||||
final Walls sut = new Walls();
|
final Walls sut = new Walls();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
Stream<Direction> result = sut.getUnsealedSet();
|
EnumSet<Direction> result = sut.getUnsealedSet();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
assertThat(result).isEmpty();
|
assertThat(result).isEmpty();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package ch.fritteli.maze.generator.serialization.v2;
|
package ch.fritteli.maze.generator.serialization.v2;
|
||||||
|
|
||||||
import ch.fritteli.maze.generator.algorithm.RandomDepthFirst;
|
import ch.fritteli.maze.generator.algorithm.RandomDepthFirst;
|
||||||
import ch.fritteli.maze.generator.algorithm.Wilson;
|
import ch.fritteli.maze.generator.algorithm.wilson.Wilson;
|
||||||
import ch.fritteli.maze.generator.model.Maze;
|
import ch.fritteli.maze.generator.model.Maze;
|
||||||
import ch.fritteli.maze.generator.model.Position;
|
import ch.fritteli.maze.generator.model.Position;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package ch.fritteli.maze.generator.serialization.v3;
|
package ch.fritteli.maze.generator.serialization.v3;
|
||||||
|
|
||||||
import ch.fritteli.maze.generator.algorithm.RandomDepthFirst;
|
import ch.fritteli.maze.generator.algorithm.RandomDepthFirst;
|
||||||
import ch.fritteli.maze.generator.algorithm.Wilson;
|
import ch.fritteli.maze.generator.algorithm.wilson.Wilson;
|
||||||
import ch.fritteli.maze.generator.model.Maze;
|
import ch.fritteli.maze.generator.model.Maze;
|
||||||
import ch.fritteli.maze.generator.model.Position;
|
import ch.fritteli.maze.generator.model.Position;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -37,4 +37,13 @@ class SerializerDeserializerV3Test {
|
||||||
final Maze result = SerializerDeserializerV3.deserialize(bytes);
|
final Maze result = SerializerDeserializerV3.deserialize(bytes);
|
||||||
assertThat(result).isEqualTo(expected);
|
assertThat(result).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSerializeDeserializeLargeRandom() throws IOException {
|
||||||
|
final Maze expected = new Maze(200, 320, 3141592653589793238L);
|
||||||
|
new RandomDepthFirst(expected).run();
|
||||||
|
final byte[] bytes = SerializerDeserializerV3.serialize(expected);
|
||||||
|
final Maze result = SerializerDeserializerV3.deserialize(bytes);
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue