Merge pull request 'feature/wilson-algorithm' (#10) from feature/wilson-algorithm into master
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #10
This commit is contained in:
commit
05ee55822a
34 changed files with 1257 additions and 340 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>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
protected AbstractMazeGeneratorAlgorithm(@NotNull final Maze maze, @NotNull final String algorithmName) {
|
||||||
|
this.maze = maze;
|
||||||
|
this.random = new Random(maze.getRandomSeed());
|
||||||
|
this.maze.setAlgorithm(algorithmName);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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, "Random Depth First");
|
||||||
this.random = new Random(maze.getRandomSeed());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
|
@ -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,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;
|
||||||
|
|
|
@ -3,6 +3,7 @@ package ch.fritteli.maze.generator.model;
|
||||||
import io.vavr.control.Option;
|
import io.vavr.control.Option;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
@ -21,6 +22,9 @@ public class Maze {
|
||||||
private final Position start;
|
private final Position start;
|
||||||
@Getter
|
@Getter
|
||||||
private final Position end;
|
private final Position end;
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private String algorithm;
|
||||||
|
|
||||||
public Maze(final int width, final int height) {
|
public Maze(final int width, final int height) {
|
||||||
this(width, height, System.nanoTime());
|
this(width, height, System.nanoTime());
|
||||||
|
@ -34,7 +38,11 @@ public class Maze {
|
||||||
this(width, height, randomSeed, new Position(0, 0), new Position(width - 1, height - 1));
|
this(width, height, randomSeed, new Position(0, 0), new Position(width - 1, height - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Maze(final int width, final int height, final long randomSeed, @NotNull final Position start, @NotNull final Position end) {
|
public Maze(final int width,
|
||||||
|
final int height,
|
||||||
|
final long randomSeed,
|
||||||
|
@NotNull final Position start,
|
||||||
|
@NotNull final Position end) {
|
||||||
if (width <= 1 || height <= 1) {
|
if (width <= 1 || height <= 1) {
|
||||||
throw new IllegalArgumentException("width and height must be >1");
|
throw new IllegalArgumentException("width and height must be >1");
|
||||||
}
|
}
|
||||||
|
@ -71,7 +79,12 @@ public class Maze {
|
||||||
/**
|
/**
|
||||||
* INTERNAL API. Exists only for deserialization. Not to be called from user code.
|
* INTERNAL API. Exists only for deserialization. Not to be called from user code.
|
||||||
*/
|
*/
|
||||||
private Maze(@NotNull final Tile[][] field, final int width, final int height, @NotNull final Position start, @NotNull final Position end, final long randomSeed) {
|
private Maze(@NotNull final Tile[][] field,
|
||||||
|
final int width,
|
||||||
|
final int height,
|
||||||
|
@NotNull final Position start,
|
||||||
|
@NotNull final Position end,
|
||||||
|
final long randomSeed) {
|
||||||
this.field = field;
|
this.field = field;
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
|
@ -80,6 +93,25 @@ public class Maze {
|
||||||
this.end = end;
|
this.end = end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* INTERNAL API. Exists only for deserialization. Not to be called from user code.
|
||||||
|
*/
|
||||||
|
private Maze(@NotNull final Tile[][] field,
|
||||||
|
final int width,
|
||||||
|
final int height,
|
||||||
|
@NotNull final Position start,
|
||||||
|
@NotNull final Position end,
|
||||||
|
final long randomSeed,
|
||||||
|
@NotNull final String algorithm) {
|
||||||
|
this.field = field;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.randomSeed = randomSeed;
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public Option<Tile> getTileAt(@NotNull final Position position) {
|
public Option<Tile> getTileAt(@NotNull final Position position) {
|
||||||
return this.getTileAt(position.x(), position.y());
|
return this.getTileAt(position.x(), position.y());
|
||||||
|
|
|
@ -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();
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,108 +6,109 @@ import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class HTMLRenderer implements Renderer<String> {
|
public class HTMLRenderer implements Renderer<String> {
|
||||||
|
|
||||||
private static final String POSTAMBLE = "<script>"
|
private static final String POSTAMBLE = """
|
||||||
+ "let userPath = [];"
|
<script>
|
||||||
+ "const DIR_UNDEF = -1;"
|
let userPath = [];
|
||||||
+ "const DIR_SAME = 0;"
|
const DIR_UNDEF = -1;
|
||||||
+ "const DIR_UP = 1;"
|
const DIR_SAME = 0;
|
||||||
+ "const DIR_RIGHT = 2;"
|
const DIR_UP = 1;
|
||||||
+ "const DIR_DOWN = 3;"
|
const DIR_RIGHT = 2;
|
||||||
+ "const DIR_LEFT = 4;"
|
const DIR_DOWN = 3;
|
||||||
+ "function getCoords(cell) {"
|
const DIR_LEFT = 4;
|
||||||
+ " return {"
|
function getCoords(cell) {
|
||||||
+ " x: cell.cellIndex,"
|
return {
|
||||||
+ " y: cell.parentElement.rowIndex"
|
x: cell.cellIndex,
|
||||||
+ " };"
|
y: cell.parentElement.rowIndex
|
||||||
+ "}"
|
};
|
||||||
+ "function distance(prev, next) {"
|
}
|
||||||
+ " return Math.abs(prev.x - next.x) + Math.abs(prev.y - next.y);"
|
function distance(prev, next) {
|
||||||
+ "}"
|
return Math.abs(prev.x - next.x) + Math.abs(prev.y - next.y);
|
||||||
+ "function direction(prev, next) {"
|
}
|
||||||
+ " const dist = distance(prev, next);"
|
function direction(prev, next) {
|
||||||
+ " if (dist === 0) {"
|
const dist = distance(prev, next);
|
||||||
+ " return DIR_SAME;"
|
if (dist === 0) {
|
||||||
+ " }"
|
return DIR_SAME;
|
||||||
+ " if (dist !== 1) {"
|
}
|
||||||
+ " return DIR_UNDEF;"
|
if (dist !== 1) {
|
||||||
+ " }"
|
return DIR_UNDEF;
|
||||||
+ " if (next.x === prev.x) {"
|
}
|
||||||
+ " if (next.y === prev.y + 1) {"
|
if (next.x === prev.x) {
|
||||||
+ " return DIR_DOWN;"
|
if (next.y === prev.y + 1) {
|
||||||
+ " }"
|
return DIR_DOWN;
|
||||||
+ " return DIR_UP;"
|
}
|
||||||
+ " }"
|
return DIR_UP;
|
||||||
+ " if (next.x === prev.x + 1) {"
|
}
|
||||||
+ " return DIR_RIGHT;"
|
if (next.x === prev.x + 1) {
|
||||||
+ " }"
|
return DIR_RIGHT;
|
||||||
+ " return DIR_LEFT;"
|
}
|
||||||
+ "}"
|
return DIR_LEFT;
|
||||||
+ "(function () {"
|
}
|
||||||
+ " const labyrinthTable = document.getElementById(\"labyrinth\");"
|
(function () {
|
||||||
+ " const labyrinthCells = labyrinthTable.getElementsByTagName(\"td\");"
|
const labyrinthTable = document.getElementById("labyrinth");
|
||||||
+ " const start = {x: 0, y: 0};"
|
const labyrinthCells = labyrinthTable.getElementsByTagName("td");
|
||||||
+ " const end = {"
|
const start = {x: 0, y: 0};
|
||||||
+ " x: labyrinthTable.getElementsByTagName(\"tr\")[0].getElementsByTagName(\"td\").length - 1,"
|
const end = {
|
||||||
+ " y: labyrinthTable.getElementsByTagName(\"tr\").length - 1"
|
x: labyrinthTable.getElementsByTagName("tr")[0].getElementsByTagName("td").length - 1,
|
||||||
+ " };"
|
y: labyrinthTable.getElementsByTagName("tr").length - 1
|
||||||
+ " for (let i = 0; i < labyrinthCells.length; i++) {"
|
};
|
||||||
+ " let cell = labyrinthCells.item(i);"
|
for (let i = 0; i < labyrinthCells.length; i++) {
|
||||||
+ " cell.onclick = (event) => {"
|
let cell = labyrinthCells.item(i);
|
||||||
+ " let target = event.target;"
|
cell.onclick = (event) => {
|
||||||
+ " const coords = getCoords(target);"
|
let target = event.target;
|
||||||
+ " if (coords.x === end.x && coords.y === end.y) {"
|
const coords = getCoords(target);
|
||||||
+ " alert(\"HOORAY! You did it! Congratulations!\")"
|
if (coords.x === end.x && coords.y === end.y) {
|
||||||
+ " }"
|
alert("HOORAY! You did it! Congratulations!")
|
||||||
+ " if (userPath.length === 0) {"
|
}
|
||||||
+ " if (coords.x === start.x && coords.y === start.y) {"
|
if (userPath.length === 0) {
|
||||||
+ " userPath.push(coords);"
|
if (coords.x === start.x && coords.y === start.y) {
|
||||||
+ " target.classList.toggle(\"user\");"
|
userPath.push(coords);
|
||||||
+ " }"
|
target.classList.toggle("user");
|
||||||
+ " } else {"
|
}
|
||||||
+ " const dir = direction(userPath[userPath.length - 1], coords);"
|
} else {
|
||||||
+ " switch (dir) {"
|
const dir = direction(userPath[userPath.length - 1], coords);
|
||||||
+ " case DIR_UNDEF:"
|
switch (dir) {
|
||||||
+ " return;"
|
case DIR_UNDEF:
|
||||||
+ " case DIR_SAME:"
|
return;
|
||||||
+ " userPath.pop();"
|
case DIR_SAME:
|
||||||
+ " target.classList.toggle(\"user\");"
|
userPath.pop();
|
||||||
+ " return;"
|
target.classList.toggle("user");
|
||||||
+ " default:"
|
return;
|
||||||
+ " if (userPath.find(value => value.x === coords.x && value.y === coords.y)) {"
|
default:
|
||||||
+ " return;"
|
if (userPath.find(value => value.x === coords.x && value.y === coords.y)) {
|
||||||
+ " } else {"
|
return;
|
||||||
+ " switch (dir) {"
|
} else {
|
||||||
+ " case DIR_UP:"
|
switch (dir) {
|
||||||
+ " if (target.classList.contains(\"bottom\")) {"
|
case DIR_UP:
|
||||||
+ " return;"
|
if (target.classList.contains("bottom")) {
|
||||||
+ " }"
|
return;
|
||||||
+ " break;"
|
}
|
||||||
+ " case DIR_RIGHT:"
|
break;
|
||||||
+ " if (target.classList.contains(\"left\")) {"
|
case DIR_RIGHT:
|
||||||
+ " return;"
|
if (target.classList.contains("left")) {
|
||||||
+ " }"
|
return;
|
||||||
+ " break;"
|
}
|
||||||
+ " case DIR_DOWN:"
|
break;
|
||||||
+ " if (target.classList.contains(\"top\")) {"
|
case DIR_DOWN:
|
||||||
+ " return;"
|
if (target.classList.contains("top")) {
|
||||||
+ " }"
|
return;
|
||||||
+ " break;"
|
}
|
||||||
+ " case DIR_LEFT:"
|
break;
|
||||||
+ " if (target.classList.contains(\"right\")) {"
|
case DIR_LEFT:
|
||||||
+ " return;"
|
if (target.classList.contains("right")) {
|
||||||
+ " }"
|
return;
|
||||||
+ " break;"
|
}
|
||||||
+ " }"
|
break;
|
||||||
+ " userPath.push(coords);"
|
}
|
||||||
+ " target.classList.toggle(\"user\");"
|
userPath.push(coords);
|
||||||
+ " return;"
|
target.classList.toggle("user");
|
||||||
+ " }"
|
return;
|
||||||
+ " }"
|
}
|
||||||
+ " }"
|
}
|
||||||
+ " };"
|
}
|
||||||
+ " }"
|
};
|
||||||
+ "})();"
|
}
|
||||||
+ "</script></body></html>";
|
})();
|
||||||
|
</script></body></html>""";
|
||||||
|
|
||||||
private HTMLRenderer() {
|
private HTMLRenderer() {
|
||||||
}
|
}
|
||||||
|
@ -135,33 +136,42 @@ public class HTMLRenderer implements Renderer<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getPreamble(@NotNull final Maze maze) {
|
private String getPreamble(@NotNull final Maze maze) {
|
||||||
return "<!DOCTYPE html><html lang=\"en\">" +
|
return """
|
||||||
"<head>" +
|
<!DOCTYPE html>
|
||||||
"<title>Maze " + maze.getWidth() + "x" + maze.getHeight() + ", ID " + maze.getRandomSeed() + "</title>" +
|
<html lang="en">
|
||||||
"<meta charset=\"utf-8\">" +
|
<head>
|
||||||
"<style>" +
|
<title>Maze %dx%d, ID %d, Algorithm %s</title>
|
||||||
"table{border-collapse:collapse;}" +
|
<meta charset="utf-8">
|
||||||
"td{border:0 solid black;height:1em;width:1em;cursor:pointer;}" +
|
<style>
|
||||||
"td.top{border-top-width:1px;}" +
|
table{border-collapse:collapse;}
|
||||||
"td.right{border-right-width:1px;}" +
|
td{border:0 solid black;height:1em;width:1em;cursor:pointer;}
|
||||||
"td.bottom{border-bottom-width:1px;}" +
|
td.top{border-top-width:1px;}
|
||||||
"td.left{border-left-width:1px;}" +
|
td.right{border-right-width:1px;}
|
||||||
"td.user{background:hotpink;}" +
|
td.bottom{border-bottom-width:1px;}
|
||||||
"</style>" +
|
td.left{border-left-width:1px;}
|
||||||
"<script>" +
|
td.user{background:hotpink;}
|
||||||
"let solution = false;" +
|
</style>
|
||||||
"function toggleSolution() {" +
|
<script>
|
||||||
"let stylesheet = document.styleSheets[0];" +
|
let solution = false;
|
||||||
"if(solution){" +
|
function toggleSolution() {
|
||||||
"stylesheet.deleteRule(0);" +
|
let stylesheet = document.styleSheets[0];
|
||||||
"}else{" +
|
if (solution) {
|
||||||
"stylesheet.insertRule(\"td.solution{background-color:lightgray;}\", 0);" +
|
stylesheet.deleteRule(0);
|
||||||
"}" +
|
} else {
|
||||||
"solution = !solution;" +
|
stylesheet.insertRule("td.solution{background-color:lightgray;}", 0);
|
||||||
"}" +
|
}
|
||||||
"</script>" +
|
solution = !solution;
|
||||||
"</head>" +
|
}
|
||||||
"<body>" +
|
</script>
|
||||||
"<input id=\"solutionbox\" type=\"checkbox\" onclick=\"toggleSolution()\"/><label for=\"solutionbox\">show solution</label>";
|
</head>
|
||||||
|
<body>
|
||||||
|
<input id="solutionbox" type="checkbox" onclick="toggleSolution()"/>
|
||||||
|
<label for="solutionbox">show solution</label>"""
|
||||||
|
.formatted(
|
||||||
|
maze.getWidth(),
|
||||||
|
maze.getHeight(),
|
||||||
|
maze.getRandomSeed(),
|
||||||
|
maze.getAlgorithm()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ class Generator {
|
||||||
JsonMaze generate() {
|
JsonMaze generate() {
|
||||||
final JsonMaze result = new JsonMaze();
|
final JsonMaze result = new JsonMaze();
|
||||||
result.setId(String.valueOf(this.maze.getRandomSeed()));
|
result.setId(String.valueOf(this.maze.getRandomSeed()));
|
||||||
|
result.setAlgorithm(this.maze.getAlgorithm());
|
||||||
result.setWidth(this.maze.getWidth());
|
result.setWidth(this.maze.getWidth());
|
||||||
result.setHeight(this.maze.getHeight());
|
result.setHeight(this.maze.getHeight());
|
||||||
final List<List<JsonCell>> rows = new ArrayList<>();
|
final List<List<JsonCell>> rows = new ArrayList<>();
|
||||||
|
|
|
@ -34,7 +34,7 @@ class Generator {
|
||||||
|
|
||||||
final PDDocument pdDocument = new PDDocument();
|
final PDDocument pdDocument = new PDDocument();
|
||||||
final PDDocumentInformation info = new PDDocumentInformation();
|
final PDDocumentInformation info = new PDDocumentInformation();
|
||||||
info.setTitle("Maze %sx%s, ID %s".formatted(this.maze.getWidth(), this.maze.getHeight(), this.maze.getRandomSeed()));
|
info.setTitle("Maze %sx%s, ID %s (%s)".formatted(this.maze.getWidth(), this.maze.getHeight(), this.maze.getRandomSeed(), this.maze.getAlgorithm()));
|
||||||
pdDocument.setDocumentInformation(info);
|
pdDocument.setDocumentInformation(info);
|
||||||
final PDPage puzzlePage = new PDPage(new PDRectangle(pageWidth, pageHeight));
|
final PDPage puzzlePage = new PDPage(new PDRectangle(pageWidth, pageHeight));
|
||||||
final PDPage solutionPage = new PDPage(new PDRectangle(pageWidth, pageHeight));
|
final PDPage solutionPage = new PDPage(new PDRectangle(pageWidth, pageHeight));
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
public abstract class AbstractMazeInputStream extends ByteArrayInputStream {
|
public abstract class AbstractMazeInputStream extends ByteArrayInputStream {
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ public abstract class AbstractMazeInputStream extends ByteArrayInputStream {
|
||||||
public abstract void checkHeader();
|
public abstract void checkHeader();
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public abstract Maze readMazeData();
|
public abstract Maze readMazeData() throws IOException;
|
||||||
|
|
||||||
public byte readByte() {
|
public byte readByte() {
|
||||||
final int read = this.read();
|
final int read = this.read();
|
||||||
|
@ -25,6 +26,10 @@ public abstract class AbstractMazeInputStream extends ByteArrayInputStream {
|
||||||
return (byte) read;
|
return (byte) read;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int readByteAsInt() {
|
||||||
|
return 0xff & this.readByte();
|
||||||
|
}
|
||||||
|
|
||||||
public int readInt() {
|
public int readInt() {
|
||||||
int result = 0;
|
int result = 0;
|
||||||
result |= (0xff & this.readByte()) << 24;
|
result |= (0xff & this.readByte()) << 24;
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
package ch.fritteli.maze.generator.serialization;
|
||||||
|
|
||||||
|
import ch.fritteli.maze.generator.model.Direction;
|
||||||
|
import ch.fritteli.maze.generator.model.Tile;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binary format description of a {@link Tile}.<br>
|
||||||
|
* A tile is stored in one byte:
|
||||||
|
* <ul>
|
||||||
|
* <li>bits 0..2: always 0</li>
|
||||||
|
* <li>bit 3: 1=solution, 0=not solution</li>
|
||||||
|
* <li>bits 4..7: encode walls</li>
|
||||||
|
* </ul>
|
||||||
|
* The values for bits 4..7 are as follows:
|
||||||
|
* <pre>
|
||||||
|
* decimal hex bin border
|
||||||
|
* 0 0 0000 no border
|
||||||
|
* 1 1 0001 top
|
||||||
|
* 2 2 0010 right
|
||||||
|
* 3 3 0011 top+right
|
||||||
|
* 4 4 0100 bottom
|
||||||
|
* 5 5 0101 top+bottom
|
||||||
|
* 6 6 0110 right+bottom
|
||||||
|
* 7 7 0111 top+right+bottom
|
||||||
|
* 8 8 1000 left
|
||||||
|
* 9 9 1001 top+left
|
||||||
|
* 10 a 1010 right+left
|
||||||
|
* 11 b 1011 top+right+left
|
||||||
|
* 12 c 1100 bottom+left
|
||||||
|
* 13 d 1101 top+bottom+left
|
||||||
|
* 14 e 1110 right+bottom+left
|
||||||
|
* 15 f 1111 top+right+bottom+left
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@UtilityClass
|
||||||
|
public class CommonTileHandler {
|
||||||
|
private final byte TOP_BIT = 0b0000_0001;
|
||||||
|
private final byte RIGHT_BIT = 0b0000_0010;
|
||||||
|
private final byte BOTTOM_BIT = 0b0000_0100;
|
||||||
|
private final byte LEFT_BIT = 0b0000_1000;
|
||||||
|
private final byte SOLUTION_BIT = 0b0001_0000;
|
||||||
|
|
||||||
|
public byte getBitmaskForTile(@NotNull final Tile tile) {
|
||||||
|
byte bitmask = 0;
|
||||||
|
if (tile.hasWallAt(Direction.TOP)) {
|
||||||
|
bitmask |= TOP_BIT;
|
||||||
|
}
|
||||||
|
if (tile.hasWallAt(Direction.RIGHT)) {
|
||||||
|
bitmask |= RIGHT_BIT;
|
||||||
|
}
|
||||||
|
if (tile.hasWallAt(Direction.BOTTOM)) {
|
||||||
|
bitmask |= BOTTOM_BIT;
|
||||||
|
}
|
||||||
|
if (tile.hasWallAt((Direction.LEFT))) {
|
||||||
|
bitmask |= LEFT_BIT;
|
||||||
|
}
|
||||||
|
if (tile.isSolution()) {
|
||||||
|
bitmask |= SOLUTION_BIT;
|
||||||
|
}
|
||||||
|
return bitmask;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public Tile getTileForBitmask(final byte bitmask) {
|
||||||
|
final EnumSet<Direction> walls = EnumSet.noneOf(Direction.class);
|
||||||
|
if ((bitmask & TOP_BIT) == TOP_BIT) {
|
||||||
|
walls.add(Direction.TOP);
|
||||||
|
}
|
||||||
|
if ((bitmask & RIGHT_BIT) == RIGHT_BIT) {
|
||||||
|
walls.add(Direction.RIGHT);
|
||||||
|
}
|
||||||
|
if ((bitmask & BOTTOM_BIT) == BOTTOM_BIT) {
|
||||||
|
walls.add(Direction.BOTTOM);
|
||||||
|
}
|
||||||
|
if ((bitmask & LEFT_BIT) == LEFT_BIT) {
|
||||||
|
walls.add(Direction.LEFT);
|
||||||
|
}
|
||||||
|
final boolean solution = (bitmask & SOLUTION_BIT) == SOLUTION_BIT;
|
||||||
|
return createTile(walls, solution);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Tile createTile(@NotNull final EnumSet<Direction> walls, boolean solution) {
|
||||||
|
try {
|
||||||
|
final Constructor<Tile> constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE);
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
return constructor.newInstance(walls, solution);
|
||||||
|
} catch (@NotNull final NoSuchMethodException | InstantiationException | IllegalAccessException |
|
||||||
|
InvocationTargetException e) {
|
||||||
|
throw new RuntimeException("Can not deserialize Tile from maze data.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ package ch.fritteli.maze.generator.serialization.v1;
|
||||||
import ch.fritteli.maze.generator.model.Maze;
|
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.MazeConstants;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class MazeInputStreamV1 extends AbstractMazeInputStream {
|
public class MazeInputStreamV1 extends AbstractMazeInputStream {
|
||||||
|
@ -13,12 +15,15 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkHeader() {
|
public void checkHeader() {
|
||||||
|
// 00 0x1a magic
|
||||||
|
// 01 0xb1 magic
|
||||||
|
// 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();
|
||||||
|
@ -30,6 +35,10 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream {
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public Maze readMazeData() {
|
public Maze readMazeData() {
|
||||||
|
// 03..10 random seed number (long)
|
||||||
|
// 11..14 width (int)
|
||||||
|
// 15..18 height (int)
|
||||||
|
// 19.. tiles
|
||||||
final long randomSeed = this.readLong();
|
final long randomSeed = this.readLong();
|
||||||
final int width = this.readInt();
|
final int width = this.readInt();
|
||||||
final int height = this.readInt();
|
final int height = this.readInt();
|
||||||
|
@ -42,7 +51,7 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream {
|
||||||
for (int y = 0; y < height; y++) {
|
for (int y = 0; y < height; y++) {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
final byte bitmask = this.readByte();
|
final byte bitmask = this.readByte();
|
||||||
tiles[x][y] = SerializerDeserializerV1.getTileForBitmask(bitmask);
|
tiles[x][y] = CommonTileHandler.getTileForBitmask(bitmask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,19 +3,28 @@ package ch.fritteli.maze.generator.serialization.v1;
|
||||||
import ch.fritteli.maze.generator.model.Maze;
|
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.MazeConstants;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class MazeOutputStreamV1 extends AbstractMazeOutputStream {
|
public class MazeOutputStreamV1 extends AbstractMazeOutputStream {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeHeader() {
|
public void writeHeader() {
|
||||||
this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_1);
|
// 00 0x1a magic
|
||||||
this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_2);
|
// 01 0xb1 magic
|
||||||
|
// 02 0x02 version
|
||||||
|
this.writeByte(MazeConstants.MAGIC_BYTE_1);
|
||||||
|
this.writeByte(MazeConstants.MAGIC_BYTE_2);
|
||||||
this.writeByte(SerializerDeserializerV1.VERSION_BYTE);
|
this.writeByte(SerializerDeserializerV1.VERSION_BYTE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeMazeData(@NotNull final Maze maze) {
|
public void writeMazeData(@NotNull final Maze maze) {
|
||||||
|
// 03..10 random seed number (long)
|
||||||
|
// 11..14 width (int)
|
||||||
|
// 15..18 height (int)
|
||||||
|
// 19.. tiles
|
||||||
final long randomSeed = maze.getRandomSeed();
|
final long randomSeed = maze.getRandomSeed();
|
||||||
final int width = maze.getWidth();
|
final int width = maze.getWidth();
|
||||||
final int height = maze.getHeight();
|
final int height = maze.getHeight();
|
||||||
|
@ -27,7 +36,7 @@ public class MazeOutputStreamV1 extends AbstractMazeOutputStream {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
// We .get() it, because we want to crash hard if it is not available.
|
// We .get() it, because we want to crash hard if it is not available.
|
||||||
final Tile tile = maze.getTileAt(x, y).get();
|
final Tile tile = maze.getTileAt(x, y).get();
|
||||||
final byte bitmask = SerializerDeserializerV1.getBitmaskForTile(tile);
|
final byte bitmask = CommonTileHandler.getBitmaskForTile(tile);
|
||||||
this.writeByte(bitmask);
|
this.writeByte(bitmask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package ch.fritteli.maze.generator.serialization.v1;
|
package ch.fritteli.maze.generator.serialization.v1;
|
||||||
|
|
||||||
import ch.fritteli.maze.generator.model.Direction;
|
|
||||||
import ch.fritteli.maze.generator.model.Maze;
|
import ch.fritteli.maze.generator.model.Maze;
|
||||||
import ch.fritteli.maze.generator.model.Tile;
|
import ch.fritteli.maze.generator.model.Tile;
|
||||||
import lombok.experimental.UtilityClass;
|
import lombok.experimental.UtilityClass;
|
||||||
|
@ -8,53 +7,24 @@ import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.EnumSet;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
* Header bytes are:
|
||||||
* decimal hex bin border
|
|
||||||
* 0 0 0000 no border
|
|
||||||
* 1 1 0001 top
|
|
||||||
* 2 2 0010 right
|
|
||||||
* 3 3 0011 top+right
|
|
||||||
* 4 4 0100 bottom
|
|
||||||
* 5 5 0101 top+bottom
|
|
||||||
* 6 6 0110 right+bottom
|
|
||||||
* 7 7 0111 top+right+bottom
|
|
||||||
* 8 8 1000 left
|
|
||||||
* 9 9 1001 top+left
|
|
||||||
* 10 a 1010 right+left
|
|
||||||
* 11 b 1011 top+right+left
|
|
||||||
* 12 c 1100 bottom+left
|
|
||||||
* 13 d 1101 top+bottom+left
|
|
||||||
* 14 e 1110 right+bottom+left
|
|
||||||
* 15 f 1111 top+right+bottom+left
|
|
||||||
* </pre>
|
|
||||||
* ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls
|
|
||||||
* ==> first bytes are:
|
|
||||||
* <pre>
|
* <pre>
|
||||||
* byte hex meaning
|
* byte hex meaning
|
||||||
* 00 0x1a magic
|
* 00 0x1a magic
|
||||||
* 01 0xb1 magic
|
* 01 0xb1 magic
|
||||||
* 02 0x01 version (0x00 -> dev, 0x01 -> stable)
|
* 02 0x01 version (0x00 -> dev, 0x01 -> stable)
|
||||||
* 03..06 width (int)
|
* 03..10 random seed number (long)
|
||||||
* 07..10 height (int)
|
* 11..14 width (int)
|
||||||
* 11..18 random seed number (long)
|
* 15..18 height (int)
|
||||||
* 19.. tiles
|
* 19.. tiles
|
||||||
* </pre>
|
* </pre>
|
||||||
* Extraneous space (poss. last nibble) is ignored.
|
* Extraneous space (poss. last nibble) is ignored.
|
||||||
*/
|
*/
|
||||||
@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;
|
|
||||||
|
|
||||||
private final byte TOP_BIT = 0b0000_0001;
|
|
||||||
private final byte RIGHT_BIT = 0b0000_0010;
|
|
||||||
private final byte BOTTOM_BIT = 0b0000_0100;
|
|
||||||
private final byte LEFT_BIT = 0b0000_1000;
|
|
||||||
private final byte SOLUTION_BIT = 0b0001_0000;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes the {@code maze} into a byte array.
|
* Serializes the {@code maze} into a byte array.
|
||||||
|
@ -94,55 +64,4 @@ public class SerializerDeserializerV1 {
|
||||||
throw new RuntimeException("Can not deserialize Maze from maze data.", e);
|
throw new RuntimeException("Can not deserialize Maze from maze data.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private Tile createTile(@NotNull final EnumSet<Direction> walls, boolean solution) {
|
|
||||||
try {
|
|
||||||
final Constructor<Tile> constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE);
|
|
||||||
constructor.setAccessible(true);
|
|
||||||
return constructor.newInstance(walls, solution);
|
|
||||||
} catch (@NotNull final NoSuchMethodException | InstantiationException | IllegalAccessException |
|
|
||||||
InvocationTargetException e) {
|
|
||||||
throw new RuntimeException("Can not deserialize Tile from maze data.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
byte getBitmaskForTile(@NotNull final Tile tile) {
|
|
||||||
byte bitmask = 0;
|
|
||||||
if (tile.hasWallAt(Direction.TOP)) {
|
|
||||||
bitmask |= TOP_BIT;
|
|
||||||
}
|
|
||||||
if (tile.hasWallAt(Direction.RIGHT)) {
|
|
||||||
bitmask |= RIGHT_BIT;
|
|
||||||
}
|
|
||||||
if (tile.hasWallAt(Direction.BOTTOM)) {
|
|
||||||
bitmask |= BOTTOM_BIT;
|
|
||||||
}
|
|
||||||
if (tile.hasWallAt((Direction.LEFT))) {
|
|
||||||
bitmask |= LEFT_BIT;
|
|
||||||
}
|
|
||||||
if (tile.isSolution()) {
|
|
||||||
bitmask |= SOLUTION_BIT;
|
|
||||||
}
|
|
||||||
return bitmask;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
Tile getTileForBitmask(final byte bitmask) {
|
|
||||||
final EnumSet<Direction> walls = EnumSet.noneOf(Direction.class);
|
|
||||||
if ((bitmask & TOP_BIT) == TOP_BIT) {
|
|
||||||
walls.add(Direction.TOP);
|
|
||||||
}
|
|
||||||
if ((bitmask & RIGHT_BIT) == RIGHT_BIT) {
|
|
||||||
walls.add(Direction.RIGHT);
|
|
||||||
}
|
|
||||||
if ((bitmask & BOTTOM_BIT) == BOTTOM_BIT) {
|
|
||||||
walls.add(Direction.BOTTOM);
|
|
||||||
}
|
|
||||||
if ((bitmask & LEFT_BIT) == LEFT_BIT) {
|
|
||||||
walls.add(Direction.LEFT);
|
|
||||||
}
|
|
||||||
final boolean solution = (bitmask & SOLUTION_BIT) == SOLUTION_BIT;
|
|
||||||
return createTile(walls, solution);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import ch.fritteli.maze.generator.model.Maze;
|
||||||
import ch.fritteli.maze.generator.model.Position;
|
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.MazeConstants;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class MazeInputStreamV2 extends AbstractMazeInputStream {
|
public class MazeInputStreamV2 extends AbstractMazeInputStream {
|
||||||
|
@ -18,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();
|
||||||
|
@ -58,7 +60,7 @@ public class MazeInputStreamV2 extends AbstractMazeInputStream {
|
||||||
for (int y = 0; y < height; y++) {
|
for (int y = 0; y < height; y++) {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
final byte bitmask = this.readByte();
|
final byte bitmask = this.readByte();
|
||||||
tiles[x][y] = SerializerDeserializerV2.getTileForBitmask(bitmask);
|
tiles[x][y] = CommonTileHandler.getTileForBitmask(bitmask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ import ch.fritteli.maze.generator.model.Maze;
|
||||||
import ch.fritteli.maze.generator.model.Position;
|
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.MazeConstants;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class MazeOutputStreamV2 extends AbstractMazeOutputStream {
|
public class MazeOutputStreamV2 extends AbstractMazeOutputStream {
|
||||||
|
@ -13,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +47,7 @@ public class MazeOutputStreamV2 extends AbstractMazeOutputStream {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
// We .get() it, because we want to crash hard if it is not available.
|
// We .get() it, because we want to crash hard if it is not available.
|
||||||
final Tile tile = maze.getTileAt(x, y).get();
|
final Tile tile = maze.getTileAt(x, y).get();
|
||||||
final byte bitmask = SerializerDeserializerV2.getBitmaskForTile(tile);
|
final byte bitmask = CommonTileHandler.getBitmaskForTile(tile);
|
||||||
this.writeByte(bitmask);
|
this.writeByte(bitmask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package ch.fritteli.maze.generator.serialization.v2;
|
package ch.fritteli.maze.generator.serialization.v2;
|
||||||
|
|
||||||
import ch.fritteli.maze.generator.model.Direction;
|
|
||||||
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 ch.fritteli.maze.generator.model.Tile;
|
import ch.fritteli.maze.generator.model.Tile;
|
||||||
|
@ -9,29 +8,9 @@ import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.EnumSet;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
* Header bytes are:
|
||||||
* decimal hex bin border
|
|
||||||
* 0 0 0000 no border
|
|
||||||
* 1 1 0001 top
|
|
||||||
* 2 2 0010 right
|
|
||||||
* 3 3 0011 top+right
|
|
||||||
* 4 4 0100 bottom
|
|
||||||
* 5 5 0101 top+bottom
|
|
||||||
* 6 6 0110 right+bottom
|
|
||||||
* 7 7 0111 top+right+bottom
|
|
||||||
* 8 8 1000 left
|
|
||||||
* 9 9 1001 top+left
|
|
||||||
* 10 a 1010 right+left
|
|
||||||
* 11 b 1011 top+right+left
|
|
||||||
* 12 c 1100 bottom+left
|
|
||||||
* 13 d 1101 top+bottom+left
|
|
||||||
* 14 e 1110 right+bottom+left
|
|
||||||
* 15 f 1111 top+right+bottom+left
|
|
||||||
* </pre>
|
|
||||||
* ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==> first bytes are:
|
|
||||||
* <pre>
|
* <pre>
|
||||||
* byte hex meaning
|
* byte hex meaning
|
||||||
* 00 0x1a magic
|
* 00 0x1a magic
|
||||||
|
@ -50,16 +29,7 @@ import java.util.EnumSet;
|
||||||
*/
|
*/
|
||||||
@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;
|
|
||||||
|
|
||||||
private final byte TOP_BIT = 0b0000_0001;
|
|
||||||
private final byte RIGHT_BIT = 0b0000_0010;
|
|
||||||
private final byte BOTTOM_BIT = 0b0000_0100;
|
|
||||||
private final byte LEFT_BIT = 0b0000_1000;
|
|
||||||
private final byte SOLUTION_BIT = 0b0001_0000;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes the {@code maze} into a byte array.
|
* Serializes the {@code maze} into a byte array.
|
||||||
|
@ -99,55 +69,4 @@ public class SerializerDeserializerV2 {
|
||||||
throw new RuntimeException("Can not deserialize Maze from maze data.", e);
|
throw new RuntimeException("Can not deserialize Maze from maze data.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private Tile createTile(@NotNull final EnumSet<Direction> walls, boolean solution) {
|
|
||||||
try {
|
|
||||||
final Constructor<Tile> constructor = Tile.class.getDeclaredConstructor(EnumSet.class, Boolean.TYPE);
|
|
||||||
constructor.setAccessible(true);
|
|
||||||
return constructor.newInstance(walls, solution);
|
|
||||||
} catch (@NotNull final NoSuchMethodException | InstantiationException | IllegalAccessException |
|
|
||||||
InvocationTargetException e) {
|
|
||||||
throw new RuntimeException("Can not deserialize Tile from maze data.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
byte getBitmaskForTile(@NotNull final Tile tile) {
|
|
||||||
byte bitmask = 0;
|
|
||||||
if (tile.hasWallAt(Direction.TOP)) {
|
|
||||||
bitmask |= TOP_BIT;
|
|
||||||
}
|
|
||||||
if (tile.hasWallAt(Direction.RIGHT)) {
|
|
||||||
bitmask |= RIGHT_BIT;
|
|
||||||
}
|
|
||||||
if (tile.hasWallAt(Direction.BOTTOM)) {
|
|
||||||
bitmask |= BOTTOM_BIT;
|
|
||||||
}
|
|
||||||
if (tile.hasWallAt((Direction.LEFT))) {
|
|
||||||
bitmask |= LEFT_BIT;
|
|
||||||
}
|
|
||||||
if (tile.isSolution()) {
|
|
||||||
bitmask |= SOLUTION_BIT;
|
|
||||||
}
|
|
||||||
return bitmask;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
Tile getTileForBitmask(final byte bitmask) {
|
|
||||||
final EnumSet<Direction> walls = EnumSet.noneOf(Direction.class);
|
|
||||||
if ((bitmask & TOP_BIT) == TOP_BIT) {
|
|
||||||
walls.add(Direction.TOP);
|
|
||||||
}
|
|
||||||
if ((bitmask & RIGHT_BIT) == RIGHT_BIT) {
|
|
||||||
walls.add(Direction.RIGHT);
|
|
||||||
}
|
|
||||||
if ((bitmask & BOTTOM_BIT) == BOTTOM_BIT) {
|
|
||||||
walls.add(Direction.BOTTOM);
|
|
||||||
}
|
|
||||||
if ((bitmask & LEFT_BIT) == LEFT_BIT) {
|
|
||||||
walls.add(Direction.LEFT);
|
|
||||||
}
|
|
||||||
final boolean solution = (bitmask & SOLUTION_BIT) == SOLUTION_BIT;
|
|
||||||
return createTile(walls, solution);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
package ch.fritteli.maze.generator.serialization.v3;
|
||||||
|
|
||||||
|
import ch.fritteli.maze.generator.model.Maze;
|
||||||
|
import ch.fritteli.maze.generator.model.Position;
|
||||||
|
import ch.fritteli.maze.generator.model.Tile;
|
||||||
|
import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream;
|
||||||
|
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
||||||
|
import ch.fritteli.maze.generator.serialization.MazeConstants;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class MazeInputStreamV3 extends AbstractMazeInputStream {
|
||||||
|
|
||||||
|
public MazeInputStreamV3(@NotNull final byte[] buf) {
|
||||||
|
super(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkHeader() {
|
||||||
|
// 00 0x1a magic
|
||||||
|
// 01 0xb1 magic
|
||||||
|
// 02 0x03 version
|
||||||
|
final byte magic1 = this.readByte();
|
||||||
|
if (magic1 != MazeConstants.MAGIC_BYTE_1) {
|
||||||
|
throw new IllegalArgumentException("Invalid maze data.");
|
||||||
|
}
|
||||||
|
final byte magic2 = this.readByte();
|
||||||
|
if (magic2 != MazeConstants.MAGIC_BYTE_2) {
|
||||||
|
throw new IllegalArgumentException("Invalid maze data.");
|
||||||
|
}
|
||||||
|
final int version = this.readByte();
|
||||||
|
if (version != SerializerDeserializerV3.VERSION_BYTE) {
|
||||||
|
throw new IllegalArgumentException("Unknown maze data version: " + version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Maze readMazeData() throws IOException {
|
||||||
|
// 03..06 width (int)
|
||||||
|
// 07..10 height (int)
|
||||||
|
// 11..14 start-x (int)
|
||||||
|
// 15..18 start-y (int)
|
||||||
|
// 19..22 end-x (int)
|
||||||
|
// 23..26 end-y (int)
|
||||||
|
// 27..34 random seed number (long)
|
||||||
|
// 35 length of the algorithm's name (unsigned byte)
|
||||||
|
// 36..+len name (bytes of String)
|
||||||
|
// +len+1.. tiles
|
||||||
|
final int width = this.readInt();
|
||||||
|
final int height = this.readInt();
|
||||||
|
final int startX = this.readInt();
|
||||||
|
final int startY = this.readInt();
|
||||||
|
final int endX = this.readInt();
|
||||||
|
final int endY = this.readInt();
|
||||||
|
final long randomSeed = this.readLong();
|
||||||
|
final int algorithmLength = this.readByteAsInt();
|
||||||
|
|
||||||
|
final String algorithm = new String(this.readNBytes(algorithmLength), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
final Tile[][] tiles = new Tile[width][height];
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
tiles[x] = new Tile[height];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
final byte bitmask = this.readByte();
|
||||||
|
tiles[x][y] = CommonTileHandler.getTileForBitmask(bitmask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Position start = new Position(startX, startY);
|
||||||
|
final Position end = new Position(endX, endY);
|
||||||
|
return SerializerDeserializerV3.createMaze(tiles, width, height, start, end, randomSeed, algorithm);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package ch.fritteli.maze.generator.serialization.v3;
|
||||||
|
|
||||||
|
import ch.fritteli.maze.generator.model.Maze;
|
||||||
|
import ch.fritteli.maze.generator.model.Position;
|
||||||
|
import ch.fritteli.maze.generator.model.Tile;
|
||||||
|
import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream;
|
||||||
|
import ch.fritteli.maze.generator.serialization.CommonTileHandler;
|
||||||
|
import ch.fritteli.maze.generator.serialization.MazeConstants;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class MazeOutputStreamV3 extends AbstractMazeOutputStream {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeHeader() {
|
||||||
|
// 00 0x1a magic
|
||||||
|
// 01 0xb1 magic
|
||||||
|
// 02 0x03 version
|
||||||
|
this.writeByte(MazeConstants.MAGIC_BYTE_1);
|
||||||
|
this.writeByte(MazeConstants.MAGIC_BYTE_2);
|
||||||
|
this.writeByte(SerializerDeserializerV3.VERSION_BYTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeMazeData(@NotNull final Maze maze) {
|
||||||
|
// 03..06 width (int)
|
||||||
|
// 07..10 height (int)
|
||||||
|
// 11..14 start-x (int)
|
||||||
|
// 15..18 start-y (int)
|
||||||
|
// 19..22 end-x (int)
|
||||||
|
// 23..26 end-y (int)
|
||||||
|
// 27..34 random seed number (long)
|
||||||
|
// 35 length of the algorithm's name (unsigned byte)
|
||||||
|
// 36..+len name (bytes of String)
|
||||||
|
// +len+1.. tiles
|
||||||
|
final long randomSeed = maze.getRandomSeed();
|
||||||
|
final AlgorithmWrapper algorithm = this.getAlgorithmWrapper(maze.getAlgorithm());
|
||||||
|
final int width = maze.getWidth();
|
||||||
|
final int height = maze.getHeight();
|
||||||
|
final Position start = maze.getStart();
|
||||||
|
final Position end = maze.getEnd();
|
||||||
|
this.writeInt(width);
|
||||||
|
this.writeInt(height);
|
||||||
|
this.writeInt(start.x());
|
||||||
|
this.writeInt(start.y());
|
||||||
|
this.writeInt(end.x());
|
||||||
|
this.writeInt(end.y());
|
||||||
|
this.writeLong(randomSeed);
|
||||||
|
this.writeByte(algorithm.length());
|
||||||
|
this.writeBytes(algorithm.name());
|
||||||
|
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
// We .get() it, because we want to crash hard if it is not available.
|
||||||
|
final Tile tile = maze.getTileAt(x, y).get();
|
||||||
|
final byte bitmask = CommonTileHandler.getBitmaskForTile(tile);
|
||||||
|
this.writeByte(bitmask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private AlgorithmWrapper getAlgorithmWrapper(@NotNull final String algorithm) {
|
||||||
|
final byte[] bytes = algorithm.getBytes(StandardCharsets.UTF_8);
|
||||||
|
if (bytes.length < 256) {
|
||||||
|
// Phew, that's the easy case!
|
||||||
|
return new AlgorithmWrapper(bytes, (byte) bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's use a very primitive, brute-force approach
|
||||||
|
int strLen = Math.min(255, algorithm.length());
|
||||||
|
int len;
|
||||||
|
byte[] name;
|
||||||
|
do {
|
||||||
|
name = algorithm.substring(0, strLen).getBytes(StandardCharsets.UTF_8);
|
||||||
|
len = name.length;
|
||||||
|
strLen--;
|
||||||
|
} while (len > 255);
|
||||||
|
|
||||||
|
return new AlgorithmWrapper(name, (byte) len);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AlgorithmWrapper(@NotNull byte[] name, byte length) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
package ch.fritteli.maze.generator.serialization.v3;
|
||||||
|
|
||||||
|
import ch.fritteli.maze.generator.model.Maze;
|
||||||
|
import ch.fritteli.maze.generator.model.Position;
|
||||||
|
import ch.fritteli.maze.generator.model.Tile;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header bytes are:
|
||||||
|
* <pre>
|
||||||
|
* byte hex meaning
|
||||||
|
* 00 0x1a magic
|
||||||
|
* 01 0xb1 magic
|
||||||
|
* 02 0x03 version (0x00 -> dev, 0x01, 0x02 -> deprecated, 0x03 -> stable)
|
||||||
|
* 03..06 width (int)
|
||||||
|
* 07..10 height (int)
|
||||||
|
* 11..14 start-x (int)
|
||||||
|
* 15..18 start-y (int)
|
||||||
|
* 19..22 end-x (int)
|
||||||
|
* 23..26 end-y (int)
|
||||||
|
* 27..34 random seed number (long)
|
||||||
|
* 35 length of the algorithm's name (number of bytes of the Java String) (unsigned byte)
|
||||||
|
* 36..(36+len) algorithm's name (bytes of the Java String) (byte...)
|
||||||
|
* 36+len+1.. tiles
|
||||||
|
* </pre>
|
||||||
|
* Extraneous space (poss. last nibble) is ignored.
|
||||||
|
*/
|
||||||
|
@UtilityClass
|
||||||
|
public class SerializerDeserializerV3 {
|
||||||
|
public final byte VERSION_BYTE = 0x03;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes the {@code maze} into a byte array.
|
||||||
|
*
|
||||||
|
* @param maze The {@link Maze} to be serialized.
|
||||||
|
* @return The resulting byte array.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public byte[] serialize(@NotNull final Maze maze) {
|
||||||
|
final MazeOutputStreamV3 stream = new MazeOutputStreamV3();
|
||||||
|
stream.writeHeader();
|
||||||
|
stream.writeMazeData(maze);
|
||||||
|
return stream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserializes the byte array into an instance of {@link Maze}.
|
||||||
|
*
|
||||||
|
* @param bytes The byte array to be deserialized.
|
||||||
|
* @return An instance of {@link Maze}.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public Maze deserialize(@NotNull final byte[] bytes) throws IOException {
|
||||||
|
final MazeInputStreamV3 stream = new MazeInputStreamV3(bytes);
|
||||||
|
stream.checkHeader();
|
||||||
|
return stream.readMazeData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
Maze createMaze(@NotNull final Tile[][] field,
|
||||||
|
final int width,
|
||||||
|
final int height,
|
||||||
|
@NotNull final Position start,
|
||||||
|
@NotNull final Position end,
|
||||||
|
final long randomSeed,
|
||||||
|
@NotNull final String algorithm) {
|
||||||
|
try {
|
||||||
|
final Constructor<Maze> constructor = Maze.class.getDeclaredConstructor(
|
||||||
|
Tile[][].class,
|
||||||
|
Integer.TYPE,
|
||||||
|
Integer.TYPE,
|
||||||
|
Position.class,
|
||||||
|
Position.class,
|
||||||
|
Long.TYPE,
|
||||||
|
String.class
|
||||||
|
);
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
return constructor.newInstance(field, width, height, start, end, randomSeed, algorithm);
|
||||||
|
} catch (@NotNull final NoSuchMethodException | IllegalAccessException | InstantiationException |
|
||||||
|
InvocationTargetException e) {
|
||||||
|
throw new RuntimeException("Can not deserialize Maze from maze data.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
|
"algorithm",
|
||||||
"width",
|
"width",
|
||||||
"height",
|
"height",
|
||||||
"start",
|
"start",
|
||||||
|
@ -17,6 +18,10 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "64 bit precision signed integer value. Transmitted as string, because ECMAScript (browsers) don't normally handle 64 bit integers well, as the ECMAScript 'number' type is a 64 bit signed double value, leaving only 53 bits for the integer part, thus losing precision."
|
"description": "64 bit precision signed integer value. Transmitted as string, because ECMAScript (browsers) don't normally handle 64 bit integers well, as the ECMAScript 'number' type is a 64 bit signed double value, leaving only 53 bits for the integer part, thus losing precision."
|
||||||
},
|
},
|
||||||
|
"algorithm": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the algorithm used to generate the maze."
|
||||||
|
},
|
||||||
"width": {
|
"width": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 1
|
"minimum": 1
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -13,6 +13,8 @@ class SerializerDeserializerV1Test {
|
||||||
new RandomDepthFirst(expected).run();
|
new RandomDepthFirst(expected).run();
|
||||||
final byte[] bytes = SerializerDeserializerV1.serialize(expected);
|
final byte[] bytes = SerializerDeserializerV1.serialize(expected);
|
||||||
final Maze result = SerializerDeserializerV1.deserialize(bytes);
|
final Maze result = SerializerDeserializerV1.deserialize(bytes);
|
||||||
|
assertThat(result.getAlgorithm()).isNull();
|
||||||
|
expected.setAlgorithm(null);
|
||||||
assertThat(result).isEqualTo(expected);
|
assertThat(result).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +24,8 @@ class SerializerDeserializerV1Test {
|
||||||
new RandomDepthFirst(expected).run();
|
new RandomDepthFirst(expected).run();
|
||||||
final byte[] bytes = SerializerDeserializerV1.serialize(expected);
|
final byte[] bytes = SerializerDeserializerV1.serialize(expected);
|
||||||
final Maze result = SerializerDeserializerV1.deserialize(bytes);
|
final Maze result = SerializerDeserializerV1.deserialize(bytes);
|
||||||
|
assertThat(result.getAlgorithm()).isNull();
|
||||||
|
expected.setAlgorithm(null);
|
||||||
assertThat(result).isEqualTo(expected);
|
assertThat(result).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +35,8 @@ class SerializerDeserializerV1Test {
|
||||||
new RandomDepthFirst(expected).run();
|
new RandomDepthFirst(expected).run();
|
||||||
final byte[] bytes = SerializerDeserializerV1.serialize(expected);
|
final byte[] bytes = SerializerDeserializerV1.serialize(expected);
|
||||||
final Maze result = SerializerDeserializerV1.deserialize(bytes);
|
final Maze result = SerializerDeserializerV1.deserialize(bytes);
|
||||||
|
assertThat(result.getAlgorithm()).isNull();
|
||||||
|
expected.setAlgorithm(null);
|
||||||
assertThat(result).isEqualTo(expected);
|
assertThat(result).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package ch.fritteli.maze.generator.serialization.v2;
|
||||||
|
|
||||||
|
import ch.fritteli.maze.generator.algorithm.RandomDepthFirst;
|
||||||
|
import ch.fritteli.maze.generator.algorithm.wilson.Wilson;
|
||||||
|
import ch.fritteli.maze.generator.model.Maze;
|
||||||
|
import ch.fritteli.maze.generator.model.Position;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class SerializerDeserializerV2Test {
|
||||||
|
@Test
|
||||||
|
void testSerializeDeserializeTiny() throws IOException {
|
||||||
|
final Maze expected = new Maze(2, 2, 255, new Position(0, 0), new Position(1, 1));
|
||||||
|
new RandomDepthFirst(expected).run();
|
||||||
|
final byte[] bytes = SerializerDeserializerV2.serialize(expected);
|
||||||
|
final Maze result = SerializerDeserializerV2.deserialize(bytes);
|
||||||
|
assertThat(result.getAlgorithm()).isNull();
|
||||||
|
expected.setAlgorithm(null);
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSerializeDeserializeMedium() throws IOException {
|
||||||
|
final Maze expected = new Maze(20, 20, -271828182846L);
|
||||||
|
new Wilson(expected).run();
|
||||||
|
final byte[] bytes = SerializerDeserializerV2.serialize(expected);
|
||||||
|
final Maze result = SerializerDeserializerV2.deserialize(bytes);
|
||||||
|
assertThat(result.getAlgorithm()).isNull();
|
||||||
|
expected.setAlgorithm(null);
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSerializeDeserializeLarge() throws IOException {
|
||||||
|
final Maze expected = new Maze(200, 320, 3141592653589793238L);
|
||||||
|
new Wilson(expected).run();
|
||||||
|
final byte[] bytes = SerializerDeserializerV2.serialize(expected);
|
||||||
|
final Maze result = SerializerDeserializerV2.deserialize(bytes);
|
||||||
|
assertThat(result.getAlgorithm()).isNull();
|
||||||
|
expected.setAlgorithm(null);
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package ch.fritteli.maze.generator.serialization.v3;
|
||||||
|
|
||||||
|
import ch.fritteli.maze.generator.algorithm.RandomDepthFirst;
|
||||||
|
import ch.fritteli.maze.generator.algorithm.wilson.Wilson;
|
||||||
|
import ch.fritteli.maze.generator.model.Maze;
|
||||||
|
import ch.fritteli.maze.generator.model.Position;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class SerializerDeserializerV3Test {
|
||||||
|
@Test
|
||||||
|
void testSerializeDeserializeTiny() throws IOException {
|
||||||
|
final Maze expected = new Maze(2, 2, 255, new Position(0, 0), new Position(1, 1));
|
||||||
|
new RandomDepthFirst(expected).run();
|
||||||
|
final byte[] bytes = SerializerDeserializerV3.serialize(expected);
|
||||||
|
final Maze result = SerializerDeserializerV3.deserialize(bytes);
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSerializeDeserializeMedium() throws IOException {
|
||||||
|
final Maze expected = new Maze(20, 20, -271828182846L);
|
||||||
|
new Wilson(expected).run();
|
||||||
|
final byte[] bytes = SerializerDeserializerV3.serialize(expected);
|
||||||
|
final Maze result = SerializerDeserializerV3.deserialize(bytes);
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSerializeDeserializeLarge() throws IOException {
|
||||||
|
final Maze expected = new Maze(200, 320, 3141592653589793238L);
|
||||||
|
new Wilson(expected).run();
|
||||||
|
final byte[] bytes = SerializerDeserializerV3.serialize(expected);
|
||||||
|
final Maze result = SerializerDeserializerV3.deserialize(bytes);
|
||||||
|
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