From cdfa1f4476d9eef3893bf0a403947f34b3532409 Mon Sep 17 00:00:00 2001 From: Manuel Friedli <manuel@fritteli.ch> Date: Sat, 14 Dec 2024 04:30:35 +0100 Subject: [PATCH 1/6] Initial - and extremely ugly - version of Wilson's maze algorithm. --- .../AbstractMazeGeneratorAlgorithm.java | 19 ++ .../algorithm/MazeGeneratorAlgorithm.java | 5 + .../generator/algorithm/RandomDepthFirst.java | 11 +- .../maze/generator/algorithm/Wilson.java | 297 ++++++++++++++++++ .../maze/generator/model/Direction.java | 3 + .../maze/generator/model/Position.java | 18 ++ .../generator/renderer/pdf/Generator.java | 2 +- .../maze/generator/algorithm/WilsonTest.java | 15 + 8 files changed, 360 insertions(+), 10 deletions(-) create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/MazeGeneratorAlgorithm.java create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java create mode 100644 src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java b/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java new file mode 100644 index 0000000..330f9e7 --- /dev/null +++ b/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java @@ -0,0 +1,19 @@ +package ch.fritteli.maze.generator.algorithm; + +import ch.fritteli.maze.generator.model.Maze; +import org.jetbrains.annotations.NotNull; + +import java.util.Random; + +public abstract class AbstractMazeGeneratorAlgorithm implements MazeGeneratorAlgorithm { + @NotNull + protected final Maze maze; + @NotNull + protected final Random random; + + + public AbstractMazeGeneratorAlgorithm(@NotNull final Maze maze) { + this.maze = maze; + this.random = new Random(maze.getRandomSeed()); + } +} diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/MazeGeneratorAlgorithm.java b/src/main/java/ch/fritteli/maze/generator/algorithm/MazeGeneratorAlgorithm.java new file mode 100644 index 0000000..0cc091a --- /dev/null +++ b/src/main/java/ch/fritteli/maze/generator/algorithm/MazeGeneratorAlgorithm.java @@ -0,0 +1,5 @@ +package ch.fritteli.maze.generator.algorithm; + +public interface MazeGeneratorAlgorithm { + void run(); +} diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java b/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java index a65b15e..21aa2d1 100644 --- a/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java +++ b/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java @@ -10,20 +10,13 @@ import org.jetbrains.annotations.Nullable; import java.util.Deque; import java.util.LinkedList; -import java.util.Random; -public class RandomDepthFirst { - - @NotNull - private final Maze maze; - @NotNull - private final Random random; +public class RandomDepthFirst extends AbstractMazeGeneratorAlgorithm { @NotNull private final Deque<Position> positions = new LinkedList<>(); public RandomDepthFirst(@NotNull final Maze maze) { - this.maze = maze; - this.random = new Random(maze.getRandomSeed()); + super(maze); } public void run() { diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java new file mode 100644 index 0000000..86e4fd6 --- /dev/null +++ b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java @@ -0,0 +1,297 @@ +package ch.fritteli.maze.generator.algorithm; + +import ch.fritteli.maze.generator.model.Direction; +import ch.fritteli.maze.generator.model.Maze; +import ch.fritteli.maze.generator.model.Position; +import ch.fritteli.maze.generator.model.Tile; +import io.vavr.Tuple2; +import io.vavr.collection.Iterator; +import io.vavr.collection.Seq; +import io.vavr.collection.Stream; +import io.vavr.collection.Vector; +import io.vavr.control.Option; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; + +/** + * An implementation of <a href="https://en.wikipedia.org/wiki/Maze_generation_algorithm#Wilson's_algorithm">Wilson's Algorithm</a>. + */ +public class Wilson extends AbstractMazeGeneratorAlgorithm { + public Wilson(@NotNull final Maze maze) { + super(maze); + } + + @Override + public void run() { + final MyMaze myMaze = new MyMaze(this.maze.getWidth(), this.maze.getHeight()); + // 1. Initialization: pick random location, add to maze + final Position position = myMaze.getRandomAvailablePosition(this.random).get(); + myMaze.setPartOfMaze(position); + + // 2. while locations-not-in-maze exist, repeat: + + final List<MyMaze.Path> pths = new ArrayList<>(); + Position startPosition; + while ((startPosition = myMaze.getRandomAvailablePosition(this.random).getOrNull()) != null) { + final MyMaze.Path path = myMaze.createPath(startPosition); + while (true) { + final Position nextPosition = path.nextRandomPosition(this.random); + if (path.contains(nextPosition)) { + path.removeLoopUpTo(nextPosition); + } else { + path.append(nextPosition); + if (myMaze.isPartOfMaze(nextPosition)) { + myMaze.setPartOfMaze(path); + pths.add(path); + break; + } + } + } + + } + + applyToMaze(pths, maze); + solve(maze); + // 1. pick random location not in maze + // 2. perform random walk until you reach the maze (*) + // 3. add path to maze + // (*): random walk: + // 1. advance in random direction + // 2. if loop with current path is formed, remove loop, continue from last location before loop + } + + private void applyToMaze(@NotNull final List<MyMaze.Path> pths, @NotNull final Maze maze) { + pths.forEach(path -> { + final Iterator<Direction> moves = Stream.ofAll(path.path) + .sliding(2) + .flatMap(poss -> poss.head().getDirectionTo(poss.get(1))); + Position position = path.path.getFirst(); + while (moves.hasNext()) { + Tile tile = maze.getTileAt(position).get(); + final Direction move = moves.next(); + tile.digTo(move); + position = position.move(move); + maze.getTileAt(position).forEach(t -> t.digTo(move.invert())); + } + }); + Direction direction = determineDirectionForDigging(maze.getStart()); + Tile t = maze.getStartTile(); + this.digTo(t, direction); + direction = determineDirectionForDigging(maze.getEnd()); + t = maze.getEndTile(); + this.digTo(t, direction); + } + + @Nullable + private Direction determineDirectionForDigging(@NotNull final Position position) { + if (position.y() == 0) { + return Direction.TOP; + } + if (position.x() == 0) { + return Direction.LEFT; + } + if (position.y() == this.maze.getHeight() - 1) { + return Direction.BOTTOM; + } + if (position.x() == this.maze.getWidth() - 1) { + return Direction.RIGHT; + } + return null; + } + + private void digTo(@NotNull final Tile tile, @NotNull final Direction direction) { + tile.enableDiggingToOrFrom(direction); + tile.digTo(direction); + } + + private void solve(@NotNull final Maze maze) { + EnumSet<Direction>[][] remainingDirs = new EnumSet[maze.getWidth()][maze.getHeight()]; + for (int x = 0; x < remainingDirs.length; x++) { + for (int y = 0; y < remainingDirs[x].length; y++) { + remainingDirs[x][y] = EnumSet.allOf(Direction.class); + } + } + Position p = maze.getStart(); + final Direction direction = this.determineDirectionForDigging(p); + remainingDirs[p.x()][p.y()].remove(direction); + LinkedList<Position> solution = new LinkedList<>(); + solution.add(p); + while (!p.equals(maze.getEnd())) { + final Tile tile = maze.getTileAt(p).get(); + EnumSet<Direction> dirs = remainingDirs[p.x()][p.y()]; + dirs.removeIf(tile::hasWallAt); + if (dirs.isEmpty()) { + solution.pop(); + p = solution.peek(); + } else { + final Direction nextDir = dirs.iterator().next(); + final Position nextPos = p.move(nextDir); + solution.push(nextPos); + remainingDirs[p.x()][p.y()].remove(nextDir); + remainingDirs[nextPos.x()][nextPos.y()].remove(nextDir.invert()); + p = nextPos; + } + } + solution.forEach(s -> maze.getTileAt(s).forEach(Tile::setSolution)); + } + + private static class MyMaze { + private final int width; + private final int height; + private final boolean[][] partOfMaze; + private final boolean[] completeColumns; + + MyMaze(final int width, final int height) { + this.width = width; + this.height = height; + this.partOfMaze = new boolean[this.width][this.height]; + for (int x = 0; x < this.width; x++) { + this.partOfMaze[x] = new boolean[this.height]; + } + this.completeColumns = new boolean[this.width]; + } + + boolean isPartOfMaze(final int x, final int y) { + return this.partOfMaze[x][y]; + } + + boolean isPartOfMaze(@NotNull final Position position) { + return this.isPartOfMaze(position.x(), position.y()); + } + + void setPartOfMaze(final int x, final int y) { + this.partOfMaze[x][y] = true; + this.checkCompleteColumn(x); + } + + void setPartOfMaze(@NotNull final Position position) { + this.setPartOfMaze(position.x(), position.y()); + } + + void setPartOfMaze(@NotNull final Path path) { + path.path.forEach(this::setPartOfMaze); + } + + void checkCompleteColumn(final int x) { + if (this.completeColumns[x]) { + return; + } + for (int y = 0; y < this.height; y++) { + if (!this.isPartOfMaze(x, y)) { + return; + } + } + this.completeColumns[x] = true; + } + + Option<Position> getRandomAvailablePosition(@NotNull final Random random) { + final Seq<Integer> allowedColumns = Vector.ofAll(this.completeColumns) + .zipWithIndex() + .reject(Tuple2::_1) + .map(Tuple2::_2); + if (allowedColumns.isEmpty()) { + return Option.none(); + } + final int x = allowedColumns.get(random.nextInt(allowedColumns.size())); + final boolean[] column = partOfMaze[x]; + + final Seq<Integer> allowedRows = Vector.ofAll(column) + .zipWithIndex() + .reject(Tuple2::_1) + .map(Tuple2::_2); + if (allowedRows.isEmpty()) { + return Option.none(); + } + final int y = allowedRows.get(random.nextInt(allowedRows.size())); + return Option.some(new Position(x, y)); + } + + public Path createPath(@NotNull final Position position) { + return new Path(position); + } + + private class Path { + @NotNull + private final List<Position> path = new LinkedList<>(); + + Path(@NotNull final Position position) { + this.path.add(position); + } + + @NotNull + Position nextRandomPosition(@NotNull final Random random) { + final Direction direction = this.getRandomDirectionFromLastPosition(random); + return this.path.getLast().move(direction); + } + + boolean contains(@NotNull final Position position) { + return this.path.contains(position); + } + + void removeLoopUpTo(@NotNull final Position position) { + while (!this.path.removeLast().equals(position)) { + } + this.path.add(position); + } + + public void append(@NotNull final Position nextPosition) { + this.path.add(nextPosition); + } + + @NotNull + private Direction getRandomDirectionFromLastPosition(@NotNull final Random random) { + final EnumSet<Direction> validDirections = this.getValidDirectionsFromLastPosition(); + if (validDirections.isEmpty()) { + throw new IllegalStateException("WE MUST NOT GET HERE! analyze why it happened!!!"); + } + if (validDirections.size() == 1) { + return validDirections.iterator().next(); + } + final Direction[] directionArray = validDirections.toArray(Direction[]::new); + final int index = random.nextInt(directionArray.length); + return directionArray[index]; + } + + @NotNull + private EnumSet<Direction> getValidDirectionsFromLastPosition() { + final Position fromPosition = this.path.getLast(); + final EnumSet<Direction> validDirections = EnumSet.allOf(Direction.class); + if (this.path.size() > 1) { + final Position prevP = this.path.get(this.path.size() - 2); + fromPosition.getDirectionTo(prevP) + .forEach(validDirections::remove); + } + boolean canLeft = fromPosition.x() > 0; + boolean canRight = fromPosition.x() < width - 1; + boolean canUp = fromPosition.y() > 0; + boolean canDown = fromPosition.y() < height - 1; + if (!canLeft) { + validDirections.remove(Direction.LEFT); + } + if (!canRight) { + validDirections.remove(Direction.RIGHT); + } + if (!canUp) { + validDirections.remove(Direction.TOP); + } + if (!canDown) { + validDirections.remove(Direction.BOTTOM); + } + return validDirections; + } + + @Override + public String toString() { + return Stream.ofAll(this.path) + .map(position -> "(%s,%s)".formatted(position.x(), position.y())) + .mkString("Path[", "->", "]"); + } + } + } +} diff --git a/src/main/java/ch/fritteli/maze/generator/model/Direction.java b/src/main/java/ch/fritteli/maze/generator/model/Direction.java index e50ba6c..75ee1c4 100644 --- a/src/main/java/ch/fritteli/maze/generator/model/Direction.java +++ b/src/main/java/ch/fritteli/maze/generator/model/Direction.java @@ -1,11 +1,14 @@ package ch.fritteli.maze.generator.model; +import org.jetbrains.annotations.NotNull; + public enum Direction { TOP, RIGHT, BOTTOM, LEFT; + @NotNull public Direction invert() { return switch (this) { case TOP -> BOTTOM; diff --git a/src/main/java/ch/fritteli/maze/generator/model/Position.java b/src/main/java/ch/fritteli/maze/generator/model/Position.java index 4b91543..a6fa788 100644 --- a/src/main/java/ch/fritteli/maze/generator/model/Position.java +++ b/src/main/java/ch/fritteli/maze/generator/model/Position.java @@ -1,10 +1,12 @@ package ch.fritteli.maze.generator.model; +import io.vavr.control.Option; import lombok.With; import org.jetbrains.annotations.NotNull; @With public record Position(int x, int y) { + @NotNull public Position move(@NotNull final Direction direction) { return switch (direction) { case BOTTOM -> this.withY(this.y + 1); @@ -13,4 +15,20 @@ public record Position(int x, int y) { case TOP -> this.withY(this.y - 1); }; } + + @NotNull + public Option<Direction> getDirectionTo(@NotNull final Position position) { + final int xDiff = position.x - this.x; + final int yDiff = position.y - this.y; + return switch (xDiff) { + case -1 -> Option.when(yDiff == 0, Direction.LEFT); + case 0 -> switch (yDiff) { + case -1 -> Option.some(Direction.TOP); + case 1 -> Option.some(Direction.BOTTOM); + default -> Option.none(); + }; + case 1 -> Option.when(yDiff == 0, Direction.RIGHT); + default -> Option.none(); + }; + } } diff --git a/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java b/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java index 0de27a9..547f3da 100644 --- a/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java +++ b/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java @@ -233,7 +233,7 @@ class Generator { if (position.equals(previousPosition)) { continue; } - if (tileAtPosition.map(Tile::isSolution).getOrElse(false)) { + if (tileAtPosition.exists(Tile::isSolution)) { return position; } } diff --git a/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java b/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java new file mode 100644 index 0000000..8e46ba5 --- /dev/null +++ b/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java @@ -0,0 +1,15 @@ +package ch.fritteli.maze.generator.algorithm; + +import ch.fritteli.maze.generator.model.Maze; +import ch.fritteli.maze.generator.renderer.text.TextRenderer; +import org.junit.jupiter.api.Test; + +class WilsonTest { + @Test + void foo() { + final Maze maze = new Maze(50, 10, 0); + final Wilson wilson = new Wilson(maze); + wilson.run(); + System.out.println(TextRenderer.newInstance().render(maze)); + } +} From 9579066cb5e407c4d57ca8d4137d4c107fdf123d Mon Sep 17 00:00:00 2001 From: Manuel Friedli <manuel@fritteli.ch> Date: Wed, 18 Dec 2024 00:24:55 +0100 Subject: [PATCH 2/6] Add Serialization V3: Include the name of the algorithm. --- .../AbstractMazeGeneratorAlgorithm.java | 3 +- .../generator/algorithm/RandomDepthFirst.java | 2 +- .../maze/generator/algorithm/Wilson.java | 2 +- .../fritteli/maze/generator/model/Maze.java | 36 ++- .../generator/renderer/html/HTMLRenderer.java | 270 +++++++++--------- .../generator/renderer/json/Generator.java | 1 + .../generator/renderer/pdf/Generator.java | 2 +- .../AbstractMazeInputStream.java | 7 +- .../v1/SerializerDeserializerV1.java | 3 +- .../serialization/v3/MazeInputStreamV3.java | 77 +++++ .../serialization/v3/MazeOutputStreamV3.java | 84 ++++++ .../v3/SerializerDeserializerV3.java | 170 +++++++++++ src/main/resources/maze.schema.json | 5 + 13 files changed, 523 insertions(+), 139 deletions(-) create mode 100644 src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java create mode 100644 src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java create mode 100644 src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java b/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java index 330f9e7..2037f76 100644 --- a/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java +++ b/src/main/java/ch/fritteli/maze/generator/algorithm/AbstractMazeGeneratorAlgorithm.java @@ -12,8 +12,9 @@ public abstract class AbstractMazeGeneratorAlgorithm implements MazeGeneratorAlg protected final Random random; - public AbstractMazeGeneratorAlgorithm(@NotNull final Maze maze) { + protected AbstractMazeGeneratorAlgorithm(@NotNull final Maze maze, @NotNull final String algorithmName) { this.maze = maze; this.random = new Random(maze.getRandomSeed()); + this.maze.setAlgorithm(algorithmName); } } diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java b/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java index 21aa2d1..eab53a9 100644 --- a/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java +++ b/src/main/java/ch/fritteli/maze/generator/algorithm/RandomDepthFirst.java @@ -16,7 +16,7 @@ public class RandomDepthFirst extends AbstractMazeGeneratorAlgorithm { private final Deque<Position> positions = new LinkedList<>(); public RandomDepthFirst(@NotNull final Maze maze) { - super(maze); + super(maze, "Random Depth First"); } public void run() { diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java index 86e4fd6..e53acce 100644 --- a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java +++ b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java @@ -24,7 +24,7 @@ import java.util.Random; */ public class Wilson extends AbstractMazeGeneratorAlgorithm { public Wilson(@NotNull final Maze maze) { - super(maze); + super(maze, "Wilson"); } @Override diff --git a/src/main/java/ch/fritteli/maze/generator/model/Maze.java b/src/main/java/ch/fritteli/maze/generator/model/Maze.java index 14d7af6..633e1b3 100644 --- a/src/main/java/ch/fritteli/maze/generator/model/Maze.java +++ b/src/main/java/ch/fritteli/maze/generator/model/Maze.java @@ -3,6 +3,7 @@ package ch.fritteli.maze.generator.model; import io.vavr.control.Option; import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.Setter; import lombok.ToString; import org.jetbrains.annotations.NotNull; @@ -21,6 +22,9 @@ public class Maze { private final Position start; @Getter private final Position end; + @Getter + @Setter + private String algorithm; public Maze(final int width, final int height) { 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)); } - 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) { 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. */ - 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.width = width; this.height = height; @@ -80,6 +93,25 @@ public class Maze { 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 public Option<Tile> getTileAt(@NotNull final Position position) { return this.getTileAt(position.x(), position.y()); diff --git a/src/main/java/ch/fritteli/maze/generator/renderer/html/HTMLRenderer.java b/src/main/java/ch/fritteli/maze/generator/renderer/html/HTMLRenderer.java index 1242ac3..a2de552 100644 --- a/src/main/java/ch/fritteli/maze/generator/renderer/html/HTMLRenderer.java +++ b/src/main/java/ch/fritteli/maze/generator/renderer/html/HTMLRenderer.java @@ -6,108 +6,109 @@ import org.jetbrains.annotations.NotNull; public class HTMLRenderer implements Renderer<String> { - private static final String POSTAMBLE = "<script>" - + "let userPath = [];" - + "const DIR_UNDEF = -1;" - + "const DIR_SAME = 0;" - + "const DIR_UP = 1;" - + "const DIR_RIGHT = 2;" - + "const DIR_DOWN = 3;" - + "const DIR_LEFT = 4;" - + "function getCoords(cell) {" - + " return {" - + " 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 direction(prev, next) {" - + " const dist = distance(prev, next);" - + " if (dist === 0) {" - + " return DIR_SAME;" - + " }" - + " if (dist !== 1) {" - + " return DIR_UNDEF;" - + " }" - + " if (next.x === prev.x) {" - + " if (next.y === prev.y + 1) {" - + " return DIR_DOWN;" - + " }" - + " return DIR_UP;" - + " }" - + " if (next.x === prev.x + 1) {" - + " return DIR_RIGHT;" - + " }" - + " return DIR_LEFT;" - + "}" - + "(function () {" - + " const labyrinthTable = document.getElementById(\"labyrinth\");" - + " const labyrinthCells = labyrinthTable.getElementsByTagName(\"td\");" - + " const start = {x: 0, y: 0};" - + " const end = {" - + " 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);" - + " cell.onclick = (event) => {" - + " let target = event.target;" - + " const coords = getCoords(target);" - + " 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) {" - + " userPath.push(coords);" - + " target.classList.toggle(\"user\");" - + " }" - + " } else {" - + " const dir = direction(userPath[userPath.length - 1], coords);" - + " switch (dir) {" - + " case DIR_UNDEF:" - + " return;" - + " case DIR_SAME:" - + " userPath.pop();" - + " target.classList.toggle(\"user\");" - + " return;" - + " default:" - + " if (userPath.find(value => value.x === coords.x && value.y === coords.y)) {" - + " return;" - + " } else {" - + " switch (dir) {" - + " case DIR_UP:" - + " if (target.classList.contains(\"bottom\")) {" - + " return;" - + " }" - + " break;" - + " case DIR_RIGHT:" - + " if (target.classList.contains(\"left\")) {" - + " return;" - + " }" - + " break;" - + " case DIR_DOWN:" - + " if (target.classList.contains(\"top\")) {" - + " return;" - + " }" - + " break;" - + " case DIR_LEFT:" - + " if (target.classList.contains(\"right\")) {" - + " return;" - + " }" - + " break;" - + " }" - + " userPath.push(coords);" - + " target.classList.toggle(\"user\");" - + " return;" - + " }" - + " }" - + " }" - + " };" - + " }" - + "})();" - + "</script></body></html>"; + private static final String POSTAMBLE = """ + <script> + let userPath = []; + const DIR_UNDEF = -1; + const DIR_SAME = 0; + const DIR_UP = 1; + const DIR_RIGHT = 2; + const DIR_DOWN = 3; + const DIR_LEFT = 4; + function getCoords(cell) { + return { + 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 direction(prev, next) { + const dist = distance(prev, next); + if (dist === 0) { + return DIR_SAME; + } + if (dist !== 1) { + return DIR_UNDEF; + } + if (next.x === prev.x) { + if (next.y === prev.y + 1) { + return DIR_DOWN; + } + return DIR_UP; + } + if (next.x === prev.x + 1) { + return DIR_RIGHT; + } + return DIR_LEFT; + } + (function () { + const labyrinthTable = document.getElementById("labyrinth"); + const labyrinthCells = labyrinthTable.getElementsByTagName("td"); + const start = {x: 0, y: 0}; + const end = { + 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); + cell.onclick = (event) => { + let target = event.target; + const coords = getCoords(target); + 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) { + userPath.push(coords); + target.classList.toggle("user"); + } + } else { + const dir = direction(userPath[userPath.length - 1], coords); + switch (dir) { + case DIR_UNDEF: + return; + case DIR_SAME: + userPath.pop(); + target.classList.toggle("user"); + return; + default: + if (userPath.find(value => value.x === coords.x && value.y === coords.y)) { + return; + } else { + switch (dir) { + case DIR_UP: + if (target.classList.contains("bottom")) { + return; + } + break; + case DIR_RIGHT: + if (target.classList.contains("left")) { + return; + } + break; + case DIR_DOWN: + if (target.classList.contains("top")) { + return; + } + break; + case DIR_LEFT: + if (target.classList.contains("right")) { + return; + } + break; + } + userPath.push(coords); + target.classList.toggle("user"); + return; + } + } + } + }; + } + })(); + </script></body></html>"""; private HTMLRenderer() { } @@ -135,33 +136,42 @@ public class HTMLRenderer implements Renderer<String> { } private String getPreamble(@NotNull final Maze maze) { - return "<!DOCTYPE html><html lang=\"en\">" + - "<head>" + - "<title>Maze " + maze.getWidth() + "x" + maze.getHeight() + ", ID " + maze.getRandomSeed() + "</title>" + - "<meta charset=\"utf-8\">" + - "<style>" + - "table{border-collapse:collapse;}" + - "td{border:0 solid black;height:1em;width:1em;cursor:pointer;}" + - "td.top{border-top-width:1px;}" + - "td.right{border-right-width:1px;}" + - "td.bottom{border-bottom-width:1px;}" + - "td.left{border-left-width:1px;}" + - "td.user{background:hotpink;}" + - "</style>" + - "<script>" + - "let solution = false;" + - "function toggleSolution() {" + - "let stylesheet = document.styleSheets[0];" + - "if(solution){" + - "stylesheet.deleteRule(0);" + - "}else{" + - "stylesheet.insertRule(\"td.solution{background-color:lightgray;}\", 0);" + - "}" + - "solution = !solution;" + - "}" + - "</script>" + - "</head>" + - "<body>" + - "<input id=\"solutionbox\" type=\"checkbox\" onclick=\"toggleSolution()\"/><label for=\"solutionbox\">show solution</label>"; + return """ + <!DOCTYPE html> + <html lang="en"> + <head> + <title>Maze %dx%d, ID %d, Algorithm %s</title> + <meta charset="utf-8"> + <style> + table{border-collapse:collapse;} + td{border:0 solid black;height:1em;width:1em;cursor:pointer;} + td.top{border-top-width:1px;} + td.right{border-right-width:1px;} + td.bottom{border-bottom-width:1px;} + td.left{border-left-width:1px;} + td.user{background:hotpink;} + </style> + <script> + let solution = false; + function toggleSolution() { + let stylesheet = document.styleSheets[0]; + if (solution) { + stylesheet.deleteRule(0); + } else { + stylesheet.insertRule("td.solution{background-color:lightgray;}", 0); + } + solution = !solution; + } + </script> + </head> + <body> + <input id="solutionbox" type="checkbox" onclick="toggleSolution()"/> + <label for="solutionbox">show solution</label>""" + .formatted( + maze.getWidth(), + maze.getHeight(), + maze.getRandomSeed(), + maze.getAlgorithm() + ); } } diff --git a/src/main/java/ch/fritteli/maze/generator/renderer/json/Generator.java b/src/main/java/ch/fritteli/maze/generator/renderer/json/Generator.java index c08ed1b..3970732 100644 --- a/src/main/java/ch/fritteli/maze/generator/renderer/json/Generator.java +++ b/src/main/java/ch/fritteli/maze/generator/renderer/json/Generator.java @@ -23,6 +23,7 @@ class Generator { JsonMaze generate() { final JsonMaze result = new JsonMaze(); result.setId(String.valueOf(this.maze.getRandomSeed())); + result.setAlgorithm(this.maze.getAlgorithm()); result.setWidth(this.maze.getWidth()); result.setHeight(this.maze.getHeight()); final List<List<JsonCell>> rows = new ArrayList<>(); diff --git a/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java b/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java index 547f3da..a4a103d 100644 --- a/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java +++ b/src/main/java/ch/fritteli/maze/generator/renderer/pdf/Generator.java @@ -34,7 +34,7 @@ class Generator { final PDDocument pdDocument = new PDDocument(); 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); final PDPage puzzlePage = new PDPage(new PDRectangle(pageWidth, pageHeight)); final PDPage solutionPage = new PDPage(new PDRectangle(pageWidth, pageHeight)); diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/AbstractMazeInputStream.java b/src/main/java/ch/fritteli/maze/generator/serialization/AbstractMazeInputStream.java index 3803ef1..7e4ba88 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/AbstractMazeInputStream.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/AbstractMazeInputStream.java @@ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze; import org.jetbrains.annotations.NotNull; import java.io.ByteArrayInputStream; +import java.io.IOException; public abstract class AbstractMazeInputStream extends ByteArrayInputStream { @@ -14,7 +15,7 @@ public abstract class AbstractMazeInputStream extends ByteArrayInputStream { public abstract void checkHeader(); @NotNull - public abstract Maze readMazeData(); + public abstract Maze readMazeData() throws IOException; public byte readByte() { final int read = this.read(); @@ -25,6 +26,10 @@ public abstract class AbstractMazeInputStream extends ByteArrayInputStream { return (byte) read; } + public int readByteAsInt() { + return 0xff & this.readByte(); + } + public int readInt() { int result = 0; result |= (0xff & this.readByte()) << 24; diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java index 24f7358..fb4a213 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java @@ -30,8 +30,7 @@ import java.util.EnumSet; * 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: + * ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==> first bytes are: * <pre> * byte hex meaning * 00 0x1a magic diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java new file mode 100644 index 0000000..1814008 --- /dev/null +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java @@ -0,0 +1,77 @@ +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 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 != SerializerDeserializerV3.MAGIC_BYTE_1) { + throw new IllegalArgumentException("Invalid maze data."); + } + final byte magic2 = this.readByte(); + if (magic2 != SerializerDeserializerV3.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] = SerializerDeserializerV3.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); + } +} diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java new file mode 100644 index 0000000..52b154a --- /dev/null +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java @@ -0,0 +1,84 @@ +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 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(SerializerDeserializerV3.MAGIC_BYTE_1); + this.writeByte(SerializerDeserializerV3.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 = SerializerDeserializerV3.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) { + } +} diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java new file mode 100644 index 0000000..839e588 --- /dev/null +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java @@ -0,0 +1,170 @@ +package ch.fritteli.maze.generator.serialization.v3; + +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 lombok.experimental.UtilityClass; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.EnumSet; + +/** + * <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> + * ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==> first 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 { + + final byte MAGIC_BYTE_1 = 0x1a; + final byte MAGIC_BYTE_2 = (byte) 0xb1; + final byte VERSION_BYTE = 0x03; + + 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. + * + * @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); + } + } + + @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); + } +} diff --git a/src/main/resources/maze.schema.json b/src/main/resources/maze.schema.json index 4b7a57e..8fbbb05 100644 --- a/src/main/resources/maze.schema.json +++ b/src/main/resources/maze.schema.json @@ -6,6 +6,7 @@ "additionalProperties": false, "required": [ "id", + "algorithm", "width", "height", "start", @@ -17,6 +18,10 @@ "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." }, + "algorithm": { + "type": "string", + "description": "The name of the algorithm used to generate the maze." + }, "width": { "type": "integer", "minimum": 1 From 5a642b354b0c0f956c18cef8fa70893606ce9bfe Mon Sep 17 00:00:00 2001 From: Manuel Friedli <manuel@fritteli.ch> Date: Wed, 18 Dec 2024 00:45:33 +0100 Subject: [PATCH 3/6] Cleanup and refactoring: Eliminate duplicated code. --- .../serialization/CommonTileHandler.java | 100 ++++++++++++++++++ .../serialization/v1/MazeInputStreamV1.java | 10 +- .../serialization/v1/MazeOutputStreamV1.java | 10 +- .../v1/SerializerDeserializerV1.java | 86 +-------------- .../serialization/v2/MazeInputStreamV2.java | 3 +- .../serialization/v2/MazeOutputStreamV2.java | 3 +- .../v2/SerializerDeserializerV2.java | 80 +------------- .../serialization/v3/MazeInputStreamV3.java | 3 +- .../serialization/v3/MazeOutputStreamV3.java | 3 +- .../v3/SerializerDeserializerV3.java | 80 +------------- 10 files changed, 132 insertions(+), 246 deletions(-) create mode 100644 src/main/java/ch/fritteli/maze/generator/serialization/CommonTileHandler.java diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/CommonTileHandler.java b/src/main/java/ch/fritteli/maze/generator/serialization/CommonTileHandler.java new file mode 100644 index 0000000..b4051c8 --- /dev/null +++ b/src/main/java/ch/fritteli/maze/generator/serialization/CommonTileHandler.java @@ -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); + } + } +} diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java index 3495e5b..5ec5c7c 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java @@ -3,6 +3,7 @@ package ch.fritteli.maze.generator.serialization.v1; import ch.fritteli.maze.generator.model.Maze; import ch.fritteli.maze.generator.model.Tile; import ch.fritteli.maze.generator.serialization.AbstractMazeInputStream; +import ch.fritteli.maze.generator.serialization.CommonTileHandler; import org.jetbrains.annotations.NotNull; public class MazeInputStreamV1 extends AbstractMazeInputStream { @@ -13,6 +14,9 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream { @Override public void checkHeader() { + // 00 0x1a magic + // 01 0xb1 magic + // 02 0x01 version final byte magic1 = this.readByte(); if (magic1 != SerializerDeserializerV1.MAGIC_BYTE_1) { throw new IllegalArgumentException("Invalid maze data."); @@ -30,6 +34,10 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream { @NotNull @Override 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 int width = this.readInt(); final int height = this.readInt(); @@ -42,7 +50,7 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { final byte bitmask = this.readByte(); - tiles[x][y] = SerializerDeserializerV1.getTileForBitmask(bitmask); + tiles[x][y] = CommonTileHandler.getTileForBitmask(bitmask); } } diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java index 62ecf70..aa8d3c2 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java @@ -3,12 +3,16 @@ package ch.fritteli.maze.generator.serialization.v1; import ch.fritteli.maze.generator.model.Maze; import ch.fritteli.maze.generator.model.Tile; import ch.fritteli.maze.generator.serialization.AbstractMazeOutputStream; +import ch.fritteli.maze.generator.serialization.CommonTileHandler; import org.jetbrains.annotations.NotNull; public class MazeOutputStreamV1 extends AbstractMazeOutputStream { @Override public void writeHeader() { + // 00 0x1a magic + // 01 0xb1 magic + // 02 0x02 version this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_1); this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_2); this.writeByte(SerializerDeserializerV1.VERSION_BYTE); @@ -16,6 +20,10 @@ public class MazeOutputStreamV1 extends AbstractMazeOutputStream { @Override 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 int width = maze.getWidth(); final int height = maze.getHeight(); @@ -27,7 +35,7 @@ public class MazeOutputStreamV1 extends AbstractMazeOutputStream { 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 = SerializerDeserializerV1.getBitmaskForTile(tile); + final byte bitmask = CommonTileHandler.getBitmaskForTile(tile); this.writeByte(bitmask); } } diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java index fb4a213..f9f02d9 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java @@ -1,6 +1,5 @@ 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.Tile; import lombok.experimental.UtilityClass; @@ -8,37 +7,17 @@ import org.jetbrains.annotations.NotNull; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.util.EnumSet; /** - * <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> - * ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==> first bytes are: + * Header bytes are: * <pre> * byte hex meaning * 00 0x1a magic * 01 0xb1 magic * 02 0x01 version (0x00 -> dev, 0x01 -> stable) - * 03..06 width (int) - * 07..10 height (int) - * 11..18 random seed number (long) + * 03..10 random seed number (long) + * 11..14 width (int) + * 15..18 height (int) * 19.. tiles * </pre> * Extraneous space (poss. last nibble) is ignored. @@ -49,12 +28,6 @@ public class SerializerDeserializerV1 { 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. * @@ -93,55 +66,4 @@ public class SerializerDeserializerV1 { 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); - } } diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java index de982a6..bbe42b1 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java @@ -4,6 +4,7 @@ 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 org.jetbrains.annotations.NotNull; public class MazeInputStreamV2 extends AbstractMazeInputStream { @@ -58,7 +59,7 @@ public class MazeInputStreamV2 extends AbstractMazeInputStream { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { final byte bitmask = this.readByte(); - tiles[x][y] = SerializerDeserializerV2.getTileForBitmask(bitmask); + tiles[x][y] = CommonTileHandler.getTileForBitmask(bitmask); } } diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java index b2305fb..c39ad3d 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java @@ -4,6 +4,7 @@ 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 org.jetbrains.annotations.NotNull; public class MazeOutputStreamV2 extends AbstractMazeOutputStream { @@ -45,7 +46,7 @@ public class MazeOutputStreamV2 extends AbstractMazeOutputStream { 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 = SerializerDeserializerV2.getBitmaskForTile(tile); + final byte bitmask = CommonTileHandler.getBitmaskForTile(tile); this.writeByte(bitmask); } } diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java b/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java index 6e7b199..5b7753e 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java @@ -1,6 +1,5 @@ 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.Position; 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.InvocationTargetException; -import java.util.EnumSet; /** - * <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> - * ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==> first bytes are: + * Header bytes are: * <pre> * byte hex meaning * 00 0x1a magic @@ -55,12 +34,6 @@ public class SerializerDeserializerV2 { 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. * @@ -99,55 +72,4 @@ public class SerializerDeserializerV2 { 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); - } } diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java index 1814008..af809b9 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java @@ -4,6 +4,7 @@ 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 org.jetbrains.annotations.NotNull; import java.io.IOException; @@ -66,7 +67,7 @@ public class MazeInputStreamV3 extends AbstractMazeInputStream { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { final byte bitmask = this.readByte(); - tiles[x][y] = SerializerDeserializerV3.getTileForBitmask(bitmask); + tiles[x][y] = CommonTileHandler.getTileForBitmask(bitmask); } } diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java index 52b154a..891f0bb 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java @@ -4,6 +4,7 @@ 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 org.jetbrains.annotations.NotNull; import java.nio.charset.StandardCharsets; @@ -52,7 +53,7 @@ public class MazeOutputStreamV3 extends AbstractMazeOutputStream { 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 = SerializerDeserializerV3.getBitmaskForTile(tile); + final byte bitmask = CommonTileHandler.getBitmaskForTile(tile); this.writeByte(bitmask); } } diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java index 839e588..ac9375d 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java @@ -1,6 +1,5 @@ package ch.fritteli.maze.generator.serialization.v3; -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; @@ -10,29 +9,9 @@ import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.util.EnumSet; /** - * <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> - * ==> bits 0..2: always 0; bit 3: 1=solution, 0=not solution; bits 4..7: encode walls ==> first bytes are: + * Header bytes are: * <pre> * byte hex meaning * 00 0x1a magic @@ -58,12 +37,6 @@ public class SerializerDeserializerV3 { final byte MAGIC_BYTE_2 = (byte) 0xb1; final byte VERSION_BYTE = 0x03; - 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. * @@ -116,55 +89,4 @@ public class SerializerDeserializerV3 { 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); - } } From 3bf438ffb92dadeb19e42140549bcad098eeb8d2 Mon Sep 17 00:00:00 2001 From: Manuel Friedli <manuel@fritteli.ch> Date: Wed, 18 Dec 2024 01:03:53 +0100 Subject: [PATCH 4/6] Add and fix tests for the Serializers. --- .../maze/generator/algorithm/Wilson.java | 15 ++++++ .../v1/SerializerDeserializerV1Test.java | 6 +++ .../v2/SerializerDeserializerV2Test.java | 46 +++++++++++++++++++ .../v3/SerializerDeserializerV3Test.java | 40 ++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java create mode 100644 src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java index e53acce..af55d7e 100644 --- a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java +++ b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java @@ -86,6 +86,21 @@ public class Wilson extends AbstractMazeGeneratorAlgorithm { 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 diff --git a/src/test/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1Test.java b/src/test/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1Test.java index 2fd819b..4d8a392 100644 --- a/src/test/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1Test.java +++ b/src/test/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1Test.java @@ -13,6 +13,8 @@ class SerializerDeserializerV1Test { new RandomDepthFirst(expected).run(); final byte[] bytes = SerializerDeserializerV1.serialize(expected); final Maze result = SerializerDeserializerV1.deserialize(bytes); + assertThat(result.getAlgorithm()).isNull(); + expected.setAlgorithm(null); assertThat(result).isEqualTo(expected); } @@ -22,6 +24,8 @@ class SerializerDeserializerV1Test { new RandomDepthFirst(expected).run(); final byte[] bytes = SerializerDeserializerV1.serialize(expected); final Maze result = SerializerDeserializerV1.deserialize(bytes); + assertThat(result.getAlgorithm()).isNull(); + expected.setAlgorithm(null); assertThat(result).isEqualTo(expected); } @@ -31,6 +35,8 @@ class SerializerDeserializerV1Test { new RandomDepthFirst(expected).run(); final byte[] bytes = SerializerDeserializerV1.serialize(expected); final Maze result = SerializerDeserializerV1.deserialize(bytes); + assertThat(result.getAlgorithm()).isNull(); + expected.setAlgorithm(null); assertThat(result).isEqualTo(expected); } } diff --git a/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java b/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java new file mode 100644 index 0000000..c4898c7 --- /dev/null +++ b/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java @@ -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; +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); + } +} diff --git a/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java b/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java new file mode 100644 index 0000000..fde2008 --- /dev/null +++ b/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java @@ -0,0 +1,40 @@ +package ch.fritteli.maze.generator.serialization.v3; + +import ch.fritteli.maze.generator.algorithm.RandomDepthFirst; +import ch.fritteli.maze.generator.algorithm.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); + } +} From 9599be21b3eb9a965c3494930a44daec1800e69b Mon Sep 17 00:00:00 2001 From: Manuel Friedli <manuel@fritteli.ch> Date: Wed, 18 Dec 2024 01:06:33 +0100 Subject: [PATCH 5/6] Make the Wilson test actually test something. --- .../maze/generator/algorithm/WilsonTest.java | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java b/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java index 8e46ba5..a4f8c9f 100644 --- a/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java +++ b/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java @@ -4,12 +4,41 @@ 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() { - final Maze maze = new Maze(50, 10, 0); + // arrange + final Maze maze = new Maze(10, 10, 0); final Wilson wilson = new Wilson(maze); + + // act wilson.run(); - System.out.println(TextRenderer.newInstance().render(maze)); + + // assert + final String textRepresentation = TextRenderer.newInstance().render(maze); + assertThat(textRepresentation).isEqualTo(""" + ╷ ╶─────┬─────┬───┬─┐ + │ │ │ │ │ + │ ╷ ╶─┬─┴─┬─╴ ╵ ╷ │ │ + │ │ │ │ │ │ │ + │ └─┐ │ ╶─┼─┬─┬─┘ │ │ + │ │ │ │ │ │ │ │ + │ ╷ │ └─╴ │ ╵ ╵ ╶─┘ │ + │ │ │ │ │ + ├─┴─┘ ╷ ┌─┴───┐ ╷ ╶─┤ + │ │ │ │ │ │ + │ ╷ ┌─┘ └───╴ │ └─┬─┤ + │ │ │ │ │ │ + ├─┴─┘ ┌───╴ ╷ └─┐ ╵ │ + │ │ │ │ │ + │ ╶─┬─┴─╴ ┌─┘ ╶─┘ ╶─┤ + │ │ │ │ + ├─╴ ├─╴ ┌─┘ ╶─┐ ╶───┤ + │ │ │ │ │ + ├───┴─╴ └─┐ ╶─┴───┐ │ + │ │ │ │ + └─────────┴───────┘ ╵"""); } } From 5e6965fa557196d436496bacde43c9c1a5af73ee Mon Sep 17 00:00:00 2001 From: Manuel Friedli <manuel@fritteli.ch> Date: Tue, 24 Dec 2024 02:50:45 +0100 Subject: [PATCH 6/6] 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. --- pom.xml | 5 + .../maze/generator/algorithm/Wilson.java | 312 ------------------ .../algorithm/wilson/MazeSolver.java | 62 ++++ .../maze/generator/algorithm/wilson/Path.java | 91 +++++ .../algorithm/wilson/PathsBuilder.java | 131 ++++++++ .../generator/algorithm/wilson/Wilson.java | 96 ++++++ .../fritteli/maze/generator/model/Tile.java | 19 +- .../fritteli/maze/generator/model/Walls.java | 23 +- .../serialization/MazeConstants.java | 9 + .../serialization/v1/MazeInputStreamV1.java | 5 +- .../serialization/v1/MazeOutputStreamV1.java | 5 +- .../v1/SerializerDeserializerV1.java | 4 +- .../serialization/v2/MazeInputStreamV2.java | 5 +- .../serialization/v2/MazeOutputStreamV2.java | 5 +- .../v2/SerializerDeserializerV2.java | 5 +- .../serialization/v3/MazeInputStreamV3.java | 5 +- .../serialization/v3/MazeOutputStreamV3.java | 5 +- .../v3/SerializerDeserializerV3.java | 5 +- .../maze/generator/algorithm/WilsonTest.java | 44 --- .../algorithm/wilson/WilsonTest.java | 100 ++++++ .../maze/generator/model/WallsTest.java | 4 +- .../v2/SerializerDeserializerV2Test.java | 2 +- .../v3/SerializerDeserializerV3Test.java | 11 +- 23 files changed, 557 insertions(+), 396 deletions(-) delete mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/wilson/MazeSolver.java create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Path.java create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/wilson/PathsBuilder.java create mode 100644 src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Wilson.java create mode 100644 src/main/java/ch/fritteli/maze/generator/serialization/MazeConstants.java delete mode 100644 src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java create mode 100644 src/test/java/ch/fritteli/maze/generator/algorithm/wilson/WilsonTest.java diff --git a/pom.xml b/pom.xml index dcdec02..54216db 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,11 @@ <groupId>io.vavr</groupId> <artifactId>vavr</artifactId> </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <version>33.2.1-jre</version> + </dependency> <dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java b/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java deleted file mode 100644 index af55d7e..0000000 --- a/src/main/java/ch/fritteli/maze/generator/algorithm/Wilson.java +++ /dev/null @@ -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[", "->", "]"); - } - } - } -} diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/MazeSolver.java b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/MazeSolver.java new file mode 100644 index 0000000..f84a20c --- /dev/null +++ b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/MazeSolver.java @@ -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()); + } +} diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Path.java b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Path.java new file mode 100644 index 0000000..ab18ec2 --- /dev/null +++ b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Path.java @@ -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[", "->", "]"); + } +} diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/PathsBuilder.java b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/PathsBuilder.java new file mode 100644 index 0000000..098ed81 --- /dev/null +++ b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/PathsBuilder.java @@ -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)); + } +} diff --git a/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Wilson.java b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Wilson.java new file mode 100644 index 0000000..1587e6e --- /dev/null +++ b/src/main/java/ch/fritteli/maze/generator/algorithm/wilson/Wilson.java @@ -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()); + } +} diff --git a/src/main/java/ch/fritteli/maze/generator/model/Tile.java b/src/main/java/ch/fritteli/maze/generator/model/Tile.java index c18ef5d..b4428f1 100644 --- a/src/main/java/ch/fritteli/maze/generator/model/Tile.java +++ b/src/main/java/ch/fritteli/maze/generator/model/Tile.java @@ -1,6 +1,6 @@ package ch.fritteli.maze.generator.model; -import io.vavr.collection.Stream; +import io.vavr.collection.Vector; import io.vavr.control.Option; import lombok.AccessLevel; import lombok.EqualsAndHashCode; @@ -17,6 +17,7 @@ import java.util.Random; @ToString public class Tile { final Walls walls = new Walls(); + @EqualsAndHashCode.Exclude boolean visited = false; @Getter boolean solution = false; @@ -65,13 +66,23 @@ public class Tile { this.walls.set(direction); } + @NotNull + public EnumSet<Direction> getOpenDirections() { + return this.walls.getOpen(); + } + + @NotNull 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()) { return Option.none(); } - final int index = random.nextInt(availableDirections.length()); - return Option.of(availableDirections.get(index)); + if (availableDirections.size() == 1) { + 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) { diff --git a/src/main/java/ch/fritteli/maze/generator/model/Walls.java b/src/main/java/ch/fritteli/maze/generator/model/Walls.java index 8a4b340..4a899c7 100644 --- a/src/main/java/ch/fritteli/maze/generator/model/Walls.java +++ b/src/main/java/ch/fritteli/maze/generator/model/Walls.java @@ -1,21 +1,17 @@ package ch.fritteli.maze.generator.model; -import io.vavr.collection.Stream; import lombok.EqualsAndHashCode; import lombok.ToString; import org.jetbrains.annotations.NotNull; import java.util.EnumSet; -import java.util.HashSet; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; @EqualsAndHashCode @ToString public class Walls { - private final SortedSet<Direction> directions = new TreeSet<>(); - private final Set<Direction> sealed = new HashSet<>(); + private final EnumSet<Direction> directions = EnumSet.noneOf(Direction.class); + @EqualsAndHashCode.Exclude + private final EnumSet<Direction> sealed = EnumSet.noneOf(Direction.class); public void set(@NotNull final Direction direction) { this.directions.add(direction); @@ -36,9 +32,11 @@ public class Walls { return this.directions.contains(direction); } - public Stream<Direction> getUnsealedSet() { - return Stream.ofAll(this.directions) - .removeAll(this.sealed); + @NotNull + public EnumSet<Direction> getUnsealedSet() { + final EnumSet<Direction> result = EnumSet.copyOf(this.directions); + result.removeAll(this.sealed); + return result; } public void seal(@NotNull final Direction direction) { @@ -51,4 +49,9 @@ public class Walls { public void unseal(@NotNull final Direction direction) { this.sealed.remove(direction); } + + @NotNull + public EnumSet<Direction> getOpen() { + return EnumSet.complementOf(this.directions); + } } diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/MazeConstants.java b/src/main/java/ch/fritteli/maze/generator/serialization/MazeConstants.java new file mode 100644 index 0000000..4b9c200 --- /dev/null +++ b/src/main/java/ch/fritteli/maze/generator/serialization/MazeConstants.java @@ -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; +} diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java index 5ec5c7c..2532787 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeInputStreamV1.java @@ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze; 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; public class MazeInputStreamV1 extends AbstractMazeInputStream { @@ -18,11 +19,11 @@ public class MazeInputStreamV1 extends AbstractMazeInputStream { // 01 0xb1 magic // 02 0x01 version final byte magic1 = this.readByte(); - if (magic1 != SerializerDeserializerV1.MAGIC_BYTE_1) { + if (magic1 != MazeConstants.MAGIC_BYTE_1) { throw new IllegalArgumentException("Invalid maze data."); } final byte magic2 = this.readByte(); - if (magic2 != SerializerDeserializerV1.MAGIC_BYTE_2) { + if (magic2 != MazeConstants.MAGIC_BYTE_2) { throw new IllegalArgumentException("Invalid maze data."); } final int version = this.readByte(); diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java index aa8d3c2..7085b04 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/MazeOutputStreamV1.java @@ -4,6 +4,7 @@ import ch.fritteli.maze.generator.model.Maze; 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; public class MazeOutputStreamV1 extends AbstractMazeOutputStream { @@ -13,8 +14,8 @@ public class MazeOutputStreamV1 extends AbstractMazeOutputStream { // 00 0x1a magic // 01 0xb1 magic // 02 0x02 version - this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_1); - this.writeByte(SerializerDeserializerV1.MAGIC_BYTE_2); + this.writeByte(MazeConstants.MAGIC_BYTE_1); + this.writeByte(MazeConstants.MAGIC_BYTE_2); this.writeByte(SerializerDeserializerV1.VERSION_BYTE); } diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java b/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java index f9f02d9..9f4a561 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v1/SerializerDeserializerV1.java @@ -24,9 +24,7 @@ import java.lang.reflect.InvocationTargetException; */ @UtilityClass public class SerializerDeserializerV1 { - final byte MAGIC_BYTE_1 = 0x1a; - final byte MAGIC_BYTE_2 = (byte) 0xb1; - final byte VERSION_BYTE = 0x01; + public final byte VERSION_BYTE = 0x01; /** * Serializes the {@code maze} into a byte array. diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java index bbe42b1..036d636 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeInputStreamV2.java @@ -5,6 +5,7 @@ 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; public class MazeInputStreamV2 extends AbstractMazeInputStream { @@ -19,11 +20,11 @@ public class MazeInputStreamV2 extends AbstractMazeInputStream { // 01 0xb1 magic // 02 0x02 version final byte magic1 = this.readByte(); - if (magic1 != SerializerDeserializerV2.MAGIC_BYTE_1) { + if (magic1 != MazeConstants.MAGIC_BYTE_1) { throw new IllegalArgumentException("Invalid maze data."); } final byte magic2 = this.readByte(); - if (magic2 != SerializerDeserializerV2.MAGIC_BYTE_2) { + if (magic2 != MazeConstants.MAGIC_BYTE_2) { throw new IllegalArgumentException("Invalid maze data."); } final int version = this.readByte(); diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java index c39ad3d..1cb8f78 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v2/MazeOutputStreamV2.java @@ -5,6 +5,7 @@ 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; public class MazeOutputStreamV2 extends AbstractMazeOutputStream { @@ -14,8 +15,8 @@ public class MazeOutputStreamV2 extends AbstractMazeOutputStream { // 00 0x1a magic // 01 0xb1 magic // 02 0x02 version - this.writeByte(SerializerDeserializerV2.MAGIC_BYTE_1); - this.writeByte(SerializerDeserializerV2.MAGIC_BYTE_2); + this.writeByte(MazeConstants.MAGIC_BYTE_1); + this.writeByte(MazeConstants.MAGIC_BYTE_2); this.writeByte(SerializerDeserializerV2.VERSION_BYTE); } diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java b/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java index 5b7753e..99f8e22 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2.java @@ -29,10 +29,7 @@ import java.lang.reflect.InvocationTargetException; */ @UtilityClass public class SerializerDeserializerV2 { - - final byte MAGIC_BYTE_1 = 0x1a; - final byte MAGIC_BYTE_2 = (byte) 0xb1; - final byte VERSION_BYTE = 0x02; + public final byte VERSION_BYTE = 0x02; /** * Serializes the {@code maze} into a byte array. diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java index af809b9..8a0dc26 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeInputStreamV3.java @@ -5,6 +5,7 @@ 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; @@ -22,11 +23,11 @@ public class MazeInputStreamV3 extends AbstractMazeInputStream { // 01 0xb1 magic // 02 0x03 version final byte magic1 = this.readByte(); - if (magic1 != SerializerDeserializerV3.MAGIC_BYTE_1) { + if (magic1 != MazeConstants.MAGIC_BYTE_1) { throw new IllegalArgumentException("Invalid maze data."); } final byte magic2 = this.readByte(); - if (magic2 != SerializerDeserializerV3.MAGIC_BYTE_2) { + if (magic2 != MazeConstants.MAGIC_BYTE_2) { throw new IllegalArgumentException("Invalid maze data."); } final int version = this.readByte(); diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java index 891f0bb..c9da80e 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/MazeOutputStreamV3.java @@ -5,6 +5,7 @@ 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; @@ -16,8 +17,8 @@ public class MazeOutputStreamV3 extends AbstractMazeOutputStream { // 00 0x1a magic // 01 0xb1 magic // 02 0x03 version - this.writeByte(SerializerDeserializerV3.MAGIC_BYTE_1); - this.writeByte(SerializerDeserializerV3.MAGIC_BYTE_2); + this.writeByte(MazeConstants.MAGIC_BYTE_1); + this.writeByte(MazeConstants.MAGIC_BYTE_2); this.writeByte(SerializerDeserializerV3.VERSION_BYTE); } diff --git a/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java b/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java index ac9375d..1da19cc 100644 --- a/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java +++ b/src/main/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3.java @@ -32,10 +32,7 @@ import java.lang.reflect.InvocationTargetException; */ @UtilityClass public class SerializerDeserializerV3 { - - final byte MAGIC_BYTE_1 = 0x1a; - final byte MAGIC_BYTE_2 = (byte) 0xb1; - final byte VERSION_BYTE = 0x03; + public final byte VERSION_BYTE = 0x03; /** * Serializes the {@code maze} into a byte array. diff --git a/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java b/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java deleted file mode 100644 index a4f8c9f..0000000 --- a/src/test/java/ch/fritteli/maze/generator/algorithm/WilsonTest.java +++ /dev/null @@ -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(""" - ╷ ╶─────┬─────┬───┬─┐ - │ │ │ │ │ - │ ╷ ╶─┬─┴─┬─╴ ╵ ╷ │ │ - │ │ │ │ │ │ │ - │ └─┐ │ ╶─┼─┬─┬─┘ │ │ - │ │ │ │ │ │ │ │ - │ ╷ │ └─╴ │ ╵ ╵ ╶─┘ │ - │ │ │ │ │ - ├─┴─┘ ╷ ┌─┴───┐ ╷ ╶─┤ - │ │ │ │ │ │ - │ ╷ ┌─┘ └───╴ │ └─┬─┤ - │ │ │ │ │ │ - ├─┴─┘ ┌───╴ ╷ └─┐ ╵ │ - │ │ │ │ │ - │ ╶─┬─┴─╴ ┌─┘ ╶─┘ ╶─┤ - │ │ │ │ - ├─╴ ├─╴ ┌─┘ ╶─┐ ╶───┤ - │ │ │ │ │ - ├───┴─╴ └─┐ ╶─┴───┐ │ - │ │ │ │ - └─────────┴───────┘ ╵"""); - } -} diff --git a/src/test/java/ch/fritteli/maze/generator/algorithm/wilson/WilsonTest.java b/src/test/java/ch/fritteli/maze/generator/algorithm/wilson/WilsonTest.java new file mode 100644 index 0000000..4457c42 --- /dev/null +++ b/src/test/java/ch/fritteli/maze/generator/algorithm/wilson/WilsonTest.java @@ -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(""" + ╷│╶─────┬─────┬─────┐ + │╰───╮ │ │ │ + ├─╴ ╷│╶─┴─┬─╴ ├─┐ ╶─┤ + │ │╰─╮ │ │ │ │ + │ ╷ └─┐│╶─┘ ┌─┘ ╵ ╷ │ + │ │ ││ │ │ │ + │ │ ╶─┤│╶─┐ ╵ ╷ ╶─┤ │ + │ │ │╰─╮│ │ │ │ + │ ├─┐ ├─╴│└─┐ │ ╶─┼─┤ + │ │ │ │╭─╯ │ │ │ │ + ├─┘ └─┘│╶─┐ └─┤ ╶─┤ │ + │ │ │ │ │ │ + ├───┐ ╷│╶─┴───┴─┐ │ │ + │ │ │╰───╮ │ │ │ + ├─╴ └─┼─╴ ╷│╷ ┌─┴─┘ │ + │ │ │││ │╭───╮│ + │ ╷ ╶─┴─┬─┤│├─┘│╶─┐││ + │ │ │ │││╭─╯ │││ + │ ├─╴ ╷ ╵ ╵│╵│┌─╴ │││ + │ │ │ ╰─╯│ │││ + └─┴───┴───────┴───┘│╵"""); + } +} diff --git a/src/test/java/ch/fritteli/maze/generator/model/WallsTest.java b/src/test/java/ch/fritteli/maze/generator/model/WallsTest.java index 7be7269..d0f861f 100644 --- a/src/test/java/ch/fritteli/maze/generator/model/WallsTest.java +++ b/src/test/java/ch/fritteli/maze/generator/model/WallsTest.java @@ -3,6 +3,8 @@ package ch.fritteli.maze.generator.model; import io.vavr.collection.Stream; 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.assertThatExceptionOfType; @@ -113,7 +115,7 @@ class WallsTest { final Walls sut = new Walls(); // act - Stream<Direction> result = sut.getUnsealedSet(); + EnumSet<Direction> result = sut.getUnsealedSet(); // assert assertThat(result).isEmpty(); diff --git a/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java b/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java index c4898c7..a89e947 100644 --- a/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java +++ b/src/test/java/ch/fritteli/maze/generator/serialization/v2/SerializerDeserializerV2Test.java @@ -1,7 +1,7 @@ package ch.fritteli.maze.generator.serialization.v2; 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.Position; import org.junit.jupiter.api.Test; diff --git a/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java b/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java index fde2008..801c677 100644 --- a/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java +++ b/src/test/java/ch/fritteli/maze/generator/serialization/v3/SerializerDeserializerV3Test.java @@ -1,7 +1,7 @@ package ch.fritteli.maze.generator.serialization.v3; 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.Position; import org.junit.jupiter.api.Test; @@ -37,4 +37,13 @@ class SerializerDeserializerV3Test { 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); + } }